Skip to content

Commit f004d5d

Browse files
committed
fixup! feat: add PoC permission and role
1 parent be420c4 commit f004d5d

4 files changed

Lines changed: 175 additions & 0 deletions

File tree

openedx_authz/models/scopes.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.apps import apps
99
from django.conf import settings
1010
from django.db import models
11+
from opaque_keys.edx.keys import CourseKey
1112
from opaque_keys.edx.locator import LibraryLocatorV2
1213

1314
from openedx_authz.models.core import Scope
@@ -31,7 +32,26 @@ def get_content_library_model():
3132
return None
3233

3334

35+
def get_course_overview_model():
36+
"""Return the CourseOverview model class specified by settings.
37+
38+
The setting `OPENEDX_AUTHZ_COURSE_OVERVIEW_MODEL` should be an
39+
app_label.ModelName string (e.g. 'content.CourseOverview').
40+
"""
41+
COURSE_OVERVIEW_MODEL = getattr(
42+
settings,
43+
"OPENEDX_AUTHZ_COURSE_OVERVIEW_MODEL",
44+
"content.CourseOverview",
45+
)
46+
try:
47+
app_label, model_name = COURSE_OVERVIEW_MODEL.split(".")
48+
return apps.get_model(app_label, model_name, require_ready=False)
49+
except LookupError:
50+
return None
51+
52+
3453
ContentLibrary = get_content_library_model()
54+
CourseOverview = get_course_overview_model()
3555

3656

3757
class ContentLibraryScope(Scope):
@@ -75,3 +95,46 @@ def get_or_create_for_external_key(cls, scope):
7595
content_library = ContentLibrary.objects.get_by_key(library_key)
7696
scope, _ = cls.objects.get_or_create(content_library=content_library)
7797
return scope
98+
99+
100+
class CourseScope(Scope):
101+
"""Scope representing a course in the authorization system.
102+
103+
.. no_pii:
104+
"""
105+
106+
NAMESPACE = "course"
107+
108+
# Link to the actual course, if applicable. In other cases, this could be null.
109+
# Piggybacking on the existing CourseOverview model to keep the ExtendedCasbinRule up to date
110+
# by deleting the Scope, and thus the ExtendedCasbinRule, when the CourseOverview is deleted.
111+
#
112+
# When content IS available, the on_delete=CASCADE will still work at the
113+
# application level through Django's signal handlers.
114+
# Use a string reference to the external app's model so Django won't try
115+
# to import it at model import time. The migration already records the
116+
# dependency on `content` when the app is present.
117+
course_overview = models.ForeignKey(
118+
settings.OPENEDX_AUTHZ_COURSE_OVERVIEW_MODEL,
119+
on_delete=models.CASCADE,
120+
null=True,
121+
blank=True,
122+
related_name="authz_scopes",
123+
swappable=True,
124+
)
125+
126+
@classmethod
127+
def get_or_create_for_external_key(cls, scope):
128+
"""Get or create a CourseScope for the given external key.
129+
130+
Args:
131+
scope: ScopeData object with an external_key attribute containing
132+
a CourseKey string.
133+
134+
Returns:
135+
CourseScope: The Scope instance for the given CourseOverview
136+
"""
137+
course_key = CourseKey.from_string(scope.external_key)
138+
course_overview = CourseOverview.get_from_id(course_key)
139+
scope, _ = cls.objects.get_or_create(course_overview=course_overview)
140+
return scope

openedx_authz/settings/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def plugin_settings(settings):
4646
if not hasattr(settings, "OPENEDX_AUTHZ_CONTENT_LIBRARY_MODEL"):
4747
settings.OPENEDX_AUTHZ_CONTENT_LIBRARY_MODEL = "content_libraries.ContentLibrary"
4848

49+
# Set default CourseOverview model for swappable dependency
50+
if not hasattr(settings, "OPENEDX_AUTHZ_COURSE_OVERVIEW_MODEL"):
51+
settings.OPENEDX_AUTHZ_COURSE_OVERVIEW_MODEL = "content.CourseOverview"
52+
4953
# Set default CASBIN_LOG_LEVEL if not already set.
5054
# This setting defines the logging level for the Casbin enforcer.
5155
if not hasattr(settings, "CASBIN_LOG_LEVEL"):

openedx_authz/settings/test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,4 @@ def plugin_settings(settings): # pylint: disable=unused-argument
7676

7777
# Use stub model for testing instead of the real content_libraries app
7878
OPENEDX_AUTHZ_CONTENT_LIBRARY_MODEL = "stubs.ContentLibrary"
79+
OPENEDX_AUTHZ_COURSE_OVERVIEW_MODEL = "stubs.CourseOverview"

openedx_authz/tests/stubs/models.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.conf import settings
88
from django.contrib.auth.models import Group
99
from django.db import models
10+
from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
1011
from opaque_keys.edx.locator import LibraryLocatorV2
1112

1213

@@ -87,3 +88,109 @@ class ContentLibraryPermission(models.Model):
8788
def __str__(self):
8889
who = self.user.username if self.user else self.group.name
8990
return f"ContentLibraryPermission ({self.access_level} for {who})"
91+
92+
93+
class CourseOverview(models.Model):
94+
"""
95+
Model for storing and caching basic information about a course.
96+
97+
This model contains basic course metadata such as an ID, display name,
98+
image URL, and any other information that would be necessary to display
99+
a course as part of:
100+
user dashboard (enrolled courses)
101+
course catalog (courses to enroll in)
102+
course about (meta data about the course)
103+
104+
.. no_pii:
105+
"""
106+
107+
class Meta:
108+
app_label = "course_overviews"
109+
110+
# IMPORTANT: Bump this whenever you modify this model and/or add a migration.
111+
VERSION = 19
112+
113+
# Cache entry versioning.
114+
version = models.IntegerField()
115+
116+
# Course identification
117+
id = CourseKeyField(db_index=True, primary_key=True, max_length=255)
118+
_location = UsageKeyField(max_length=255)
119+
org = models.TextField(max_length=255, default="outdated_entry")
120+
display_name = models.TextField(null=True)
121+
display_number_with_default = models.TextField()
122+
display_org_with_default = models.TextField()
123+
124+
start = models.DateTimeField(null=True)
125+
end = models.DateTimeField(null=True)
126+
127+
# These are deprecated and unused, but cannot be dropped via simple migration due to the size of the downstream
128+
# history table. See DENG-19 for details.
129+
# Please use start and end above for these values.
130+
start_date = models.DateTimeField(null=True)
131+
end_date = models.DateTimeField(null=True)
132+
133+
advertised_start = models.TextField(null=True)
134+
announcement = models.DateTimeField(null=True)
135+
136+
# URLs
137+
# Not allowing null per django convention; not sure why many TextFields in this model do allow null
138+
banner_image_url = models.TextField()
139+
course_image_url = models.TextField()
140+
social_sharing_url = models.TextField(null=True)
141+
end_of_course_survey_url = models.TextField(null=True)
142+
143+
# Certification data
144+
certificates_display_behavior = models.TextField(null=True)
145+
certificates_show_before_end = models.BooleanField(default=False)
146+
cert_html_view_enabled = models.BooleanField(default=False)
147+
has_any_active_web_certificate = models.BooleanField(default=False)
148+
cert_name_short = models.TextField()
149+
cert_name_long = models.TextField()
150+
certificate_available_date = models.DateTimeField(default=None, null=True)
151+
152+
# Grading
153+
lowest_passing_grade = models.DecimalField(max_digits=5, decimal_places=2, null=True)
154+
155+
# Access parameters
156+
days_early_for_beta = models.FloatField(null=True)
157+
mobile_available = models.BooleanField(default=False)
158+
visible_to_staff_only = models.BooleanField(default=False)
159+
_pre_requisite_courses_json = models.TextField() # JSON representation of list of CourseKey strings
160+
161+
# Enrollment details
162+
enrollment_start = models.DateTimeField(null=True)
163+
enrollment_end = models.DateTimeField(null=True)
164+
enrollment_domain = models.TextField(null=True)
165+
invitation_only = models.BooleanField(default=False)
166+
max_student_enrollments_allowed = models.IntegerField(null=True)
167+
168+
# Catalog information
169+
catalog_visibility = models.TextField(null=True)
170+
short_description = models.TextField(null=True)
171+
course_video_url = models.TextField(null=True)
172+
effort = models.TextField(null=True)
173+
self_paced = models.BooleanField(default=False)
174+
marketing_url = models.TextField(null=True)
175+
eligible_for_financial_aid = models.BooleanField(default=True)
176+
177+
# Course highlight info, used to guide course update emails
178+
has_highlights = models.BooleanField(null=True, default=None) # if None, you have to look up the answer yourself
179+
180+
# Proctoring
181+
enable_proctored_exams = models.BooleanField(default=False)
182+
proctoring_provider = models.TextField(null=True)
183+
proctoring_escalation_email = models.TextField(null=True)
184+
allow_proctoring_opt_out = models.BooleanField(default=False)
185+
186+
# Entrance Exam information
187+
entrance_exam_enabled = models.BooleanField(default=False)
188+
entrance_exam_id = models.CharField(max_length=255, blank=True)
189+
entrance_exam_minimum_score_pct = models.FloatField(default=0.65)
190+
191+
# Open Response Assessment configuration
192+
force_on_flexible_peer_openassessments = models.BooleanField(default=False)
193+
194+
external_id = models.CharField(max_length=128, null=True, blank=True)
195+
196+
language = models.TextField(null=True)

0 commit comments

Comments
 (0)