|
9 | 9 |
|
10 | 10 | from attrs import define |
11 | 11 | from opaque_keys import InvalidKeyError |
| 12 | +from opaque_keys.edx.keys import CourseKey |
12 | 13 | from opaque_keys.edx.locator import LibraryLocatorV2 |
13 | 14 |
|
14 | 15 | try: |
15 | 16 | from openedx.core.djangoapps.content_libraries.models import ContentLibrary |
16 | 17 | except ImportError: |
17 | 18 | ContentLibrary = None |
18 | 19 |
|
| 20 | +try: |
| 21 | + from openedx.core.djangoapps.content.course_overviews.models import CourseOverview |
| 22 | +except ImportError: |
| 23 | + CourseOverview = None |
| 24 | + |
19 | 25 | __all__ = [ |
20 | 26 | "UserData", |
21 | 27 | "PermissionData", |
@@ -212,6 +218,8 @@ def get_subclass_by_namespaced_key(mcs, namespaced_key: str) -> Type["ScopeData" |
212 | 218 | The ScopeData subclass for the namespace, or ScopeData if namespace not recognized. |
213 | 219 |
|
214 | 220 | Examples: |
| 221 | + >>> ScopeMeta.get_subclass_by_namespaced_key('course-v1^course-v1:WGU+CS002+2025_T1') |
| 222 | + <class 'CourseOverviewData'> |
215 | 223 | >>> ScopeMeta.get_subclass_by_namespaced_key('lib^lib:DemoX:CSPROB') |
216 | 224 | <class 'ContentLibraryData'> |
217 | 225 | >>> ScopeMeta.get_subclass_by_namespaced_key('global^generic') |
@@ -462,6 +470,108 @@ def __repr__(self): |
462 | 470 | return self.namespaced_key |
463 | 471 |
|
464 | 472 |
|
| 473 | +@define |
| 474 | +class CourseOverviewData(ScopeData): |
| 475 | + """A course scope for authorization in the Open edX platform. |
| 476 | +
|
| 477 | + Courses uses the CourseKey format for identification. |
| 478 | +
|
| 479 | + Attributes: |
| 480 | + NAMESPACE: 'course-v1' for course scopes. |
| 481 | + external_key: The course identifier (e.g., 'course-v1:TestOrg+TestCourse+2024_T1'). |
| 482 | + Must be a valid CourseKey format. |
| 483 | + namespaced_key: The course identifier with namespace (e.g., 'course-v1^course-v1:TestOrg+TestCourse+2024_T1'). |
| 484 | + course_id: Property alias for external_key. |
| 485 | +
|
| 486 | + Examples: |
| 487 | + >>> course = CourseOverviewData(external_key='course-v1:TestOrg+TestCourse+2024_T1') |
| 488 | + >>> course.namespaced_key |
| 489 | + 'course-v1^course-v1:TestOrg+TestCourse+2024_T1' |
| 490 | + >>> course.course_id |
| 491 | + 'course-v1:TestOrg+TestCourse+2024_T1' |
| 492 | +
|
| 493 | + """ |
| 494 | + |
| 495 | + NAMESPACE: ClassVar[str] = "course-v1" |
| 496 | + |
| 497 | + @property |
| 498 | + def course_id(self) -> str: |
| 499 | + """The course identifier as used in Open edX (e.g., 'course-v1:TestOrg+TestCourse+2024_T1'). |
| 500 | +
|
| 501 | + This is an alias for external_key that represents the course ID without the namespace prefix. |
| 502 | +
|
| 503 | + Returns: |
| 504 | + str: The course identifier without namespace. |
| 505 | + """ |
| 506 | + return self.external_key |
| 507 | + |
| 508 | + @property |
| 509 | + def course_key(self) -> CourseKey: |
| 510 | + """The CourseKey object for the course. |
| 511 | +
|
| 512 | + Returns: |
| 513 | + CourseKey: The course key object. |
| 514 | + """ |
| 515 | + return CourseKey.from_string(self.course_id) |
| 516 | + |
| 517 | + @classmethod |
| 518 | + def validate_external_key(cls, external_key: str) -> bool: |
| 519 | + """Validate the external_key format for CourseOverviewData. |
| 520 | +
|
| 521 | + Args: |
| 522 | + external_key: The external key to validate. |
| 523 | +
|
| 524 | + Returns: |
| 525 | + bool: True if valid, False otherwise. |
| 526 | + """ |
| 527 | + try: |
| 528 | + CourseKey.from_string(external_key) |
| 529 | + return True |
| 530 | + except InvalidKeyError: |
| 531 | + return False |
| 532 | + |
| 533 | + def get_object(self) -> CourseOverview | None: |
| 534 | + """Retrieve the CourseOverview instance associated with this scope. |
| 535 | +
|
| 536 | + This method converts the course_id to a CourseKey and queries the |
| 537 | + database to fetch the corresponding CourseOverview object. |
| 538 | +
|
| 539 | + Returns: |
| 540 | + CourseOverview | None: The CourseOverview instance if found in the database, |
| 541 | + or None if the course does not exist or has an invalid key format. |
| 542 | +
|
| 543 | + Examples: |
| 544 | + >>> course_scope = CourseOverviewData(external_key='course-v1:TestOrg+TestCourse+2024_T1') |
| 545 | + >>> course_obj = course_scope.get_object() # CourseOverview object |
| 546 | + """ |
| 547 | + try: |
| 548 | + course_obj = CourseOverview.get_from_id(self.course_key) |
| 549 | + # Validate canonical key: get_by_key is case-insensitive, but we require exact match |
| 550 | + # This ensures authorization uses canonical course IDs consistently |
| 551 | + if course_obj.id != self.course_key: |
| 552 | + raise CourseOverview.DoesNotExist |
| 553 | + except (InvalidKeyError, CourseOverview.DoesNotExist): |
| 554 | + return None |
| 555 | + |
| 556 | + return course_obj |
| 557 | + |
| 558 | + def exists(self) -> bool: |
| 559 | + """Check if the course overview exists. |
| 560 | +
|
| 561 | + Returns: |
| 562 | + bool: True if the course overview exists, False otherwise. |
| 563 | + """ |
| 564 | + return self.get_object() is not None |
| 565 | + |
| 566 | + def __str__(self): |
| 567 | + """Human readable string representation of the course overview.""" |
| 568 | + return self.course_id |
| 569 | + |
| 570 | + def __repr__(self): |
| 571 | + """Developer friendly string representation of the course overview.""" |
| 572 | + return self.namespaced_key |
| 573 | + |
| 574 | + |
465 | 575 | class SubjectMeta(type): |
466 | 576 | """Metaclass for SubjectData to handle dynamic subclass instantiation based on namespace.""" |
467 | 577 |
|
|
0 commit comments