From 42fef33f06b106bf572fe0d58512ff8bdfc0fbfd Mon Sep 17 00:00:00 2001 From: Volodymyr Bergman Date: Fri, 29 Mar 2024 11:58:49 +0200 Subject: [PATCH] refactor: [ACI-502, ACI-503] redesign course grading events payload (#8) * refactor: [ACI-502, ACI-503] redesign course grading events payload * refactor: include passing status to data class * refactor: merge passing/failing signals to a single status signal * refactor: update avro schemas --- .../event_bus/avro/custom_serializers.py | 2 +- ...ccx+course+grade+now+failed+v1_schema.avsc | 87 ------------------- ...ccx+course+grade+now+passed+v1_schema.avsc | 87 ------------------- ...+ccx+course+passing+status+v1_schema.avsc} | 38 ++++++-- ...ning+course+passing+status+v1_schema.avsc} | 80 +++++++++-------- openedx_events/learning/data.py | 84 +++++++++++------- openedx_events/learning/signals.py | 58 ++++--------- 7 files changed, 152 insertions(+), 284 deletions(-) delete mode 100644 openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+grade+now+failed+v1_schema.avsc delete mode 100644 openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+grade+now+passed+v1_schema.avsc rename openedx_events/event_bus/avro/tests/schemas/{org+openedx+learning+course+grade+now+passed+v1_schema.avsc => org+openedx+learning+ccx+course+passing+status+v1_schema.avsc} (68%) rename openedx_events/event_bus/avro/tests/schemas/{org+openedx+learning+course+grade+now+failed+v1_schema.avsc => org+openedx+learning+course+passing+status+v1_schema.avsc} (83%) diff --git a/openedx_events/event_bus/avro/custom_serializers.py b/openedx_events/event_bus/avro/custom_serializers.py index 0ce6bf41..d41616c2 100644 --- a/openedx_events/event_bus/avro/custom_serializers.py +++ b/openedx_events/event_bus/avro/custom_serializers.py @@ -62,7 +62,7 @@ class CcxCourseLocatorAvroSerializer(BaseCustomTypeAvroSerializer): def serialize(obj) -> str: """Serialize obj into string.""" return str(obj) - + @staticmethod def deserialize(data: str): """Deserialize string into obj.""" diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+grade+now+failed+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+grade+now+failed+v1_schema.avsc deleted file mode 100644 index 1b6c4ad3..00000000 --- a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+grade+now+failed+v1_schema.avsc +++ /dev/null @@ -1,87 +0,0 @@ -{ - "name": "CloudEvent", - "type": "record", - "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", - "fields": [ - { - "name": "ccx_course", - "type": { - "name": "CcxCourseData", - "type": "record", - "fields": [ - { - "name": "course_key", - "type": "string" - }, - { - "name": "master_course_key", - "type": "string" - }, - { - "name": "max_students_allowed", - "type": "long" - }, - { - "name": "coach", - "type": { - "name": "UserData", - "type": "record", - "fields": [ - { - "name": "id", - "type": "long" - }, - { - "name": "is_active", - "type": "boolean" - }, - { - "name": "pii", - "type": { - "name": "UserPersonalData", - "type": "record", - "fields": [ - { - "name": "username", - "type": "string" - }, - { - "name": "email", - "type": "string" - }, - { - "name": "name", - "type": "string" - } - ] - } - } - ] - } - }, - { - "name": "display_name", - "type": "string" - }, - { - "name": "start", - "type": [ - "null", - "string" - ], - "default": null - }, - { - "name": "end", - "type": [ - "null", - "string" - ], - "default": null - } - ] - } - } - ], - "namespace": "org.openedx.learning.ccx.course.grade.now.failed.v1" -} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+grade+now+passed+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+grade+now+passed+v1_schema.avsc deleted file mode 100644 index 7bf247d6..00000000 --- a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+grade+now+passed+v1_schema.avsc +++ /dev/null @@ -1,87 +0,0 @@ -{ - "name": "CloudEvent", - "type": "record", - "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", - "fields": [ - { - "name": "ccx_course", - "type": { - "name": "CcxCourseData", - "type": "record", - "fields": [ - { - "name": "course_key", - "type": "string" - }, - { - "name": "master_course_key", - "type": "string" - }, - { - "name": "max_students_allowed", - "type": "long" - }, - { - "name": "coach", - "type": { - "name": "UserData", - "type": "record", - "fields": [ - { - "name": "id", - "type": "long" - }, - { - "name": "is_active", - "type": "boolean" - }, - { - "name": "pii", - "type": { - "name": "UserPersonalData", - "type": "record", - "fields": [ - { - "name": "username", - "type": "string" - }, - { - "name": "email", - "type": "string" - }, - { - "name": "name", - "type": "string" - } - ] - } - } - ] - } - }, - { - "name": "display_name", - "type": "string" - }, - { - "name": "start", - "type": [ - "null", - "string" - ], - "default": null - }, - { - "name": "end", - "type": [ - "null", - "string" - ], - "default": null - } - ] - } - } - ], - "namespace": "org.openedx.learning.ccx.course.grade.now.passed.v1" -} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+grade+now+passed+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+passing+status+v1_schema.avsc similarity index 68% rename from openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+grade+now+passed+v1_schema.avsc rename to openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+passing+status+v1_schema.avsc index 19534cab..5039aadf 100644 --- a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+grade+now+passed+v1_schema.avsc +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+passing+status+v1_schema.avsc @@ -4,11 +4,15 @@ "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", "fields": [ { - "name": "user_course_data", + "name": "course_passing_status", "type": { - "name": "UserCourseData", + "name": "CcxCoursePassingStatusData", "type": "record", "fields": [ + { + "name": "status", + "type": "string" + }, { "name": "user", "type": { @@ -47,20 +51,36 @@ ] } }, + { + "name": "update_timestamp", + "type": "string" + }, + { + "name": "grading_policy_hash", + "type": "string" + }, { "name": "course", "type": { - "name": "CourseData", + "name": "CcxCourseData", "type": "record", "fields": [ { - "name": "course_key", + "name": "ccx_course_key", + "type": "string" + }, + { + "name": "master_course_key", "type": "string" }, { "name": "display_name", "type": "string" }, + { + "name": "coach_email", + "type": "string" + }, { "name": "start", "type": [ @@ -76,6 +96,14 @@ "string" ], "default": null + }, + { + "name": "max_students_allowed", + "type": [ + "null", + "long" + ], + "default": null } ] } @@ -84,5 +112,5 @@ } } ], - "namespace": "org.openedx.learning.course.grade.now.passed.v1" + "namespace": "org.openedx.learning.ccx.course.passing.status.v1" } \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+grade+now+failed+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+passing+status+v1_schema.avsc similarity index 83% rename from openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+grade+now+failed+v1_schema.avsc rename to openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+passing+status+v1_schema.avsc index ade981c8..27a5c000 100644 --- a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+grade+now+failed+v1_schema.avsc +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+passing+status+v1_schema.avsc @@ -4,11 +4,48 @@ "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", "fields": [ { - "name": "user_course_data", + "name": "course_passing_status", "type": { - "name": "UserCourseData", + "name": "CoursePassingStatusData", "type": "record", "fields": [ + { + "name": "status", + "type": "string" + }, + { + "name": "course", + "type": { + "name": "CourseData", + "type": "record", + "fields": [ + { + "name": "course_key", + "type": "string" + }, + { + "name": "display_name", + "type": "string" + }, + { + "name": "start", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "end", + "type": [ + "null", + "string" + ], + "default": null + } + ] + } + }, { "name": "user", "type": { @@ -48,41 +85,16 @@ } }, { - "name": "course", - "type": { - "name": "CourseData", - "type": "record", - "fields": [ - { - "name": "course_key", - "type": "string" - }, - { - "name": "display_name", - "type": "string" - }, - { - "name": "start", - "type": [ - "null", - "string" - ], - "default": null - }, - { - "name": "end", - "type": [ - "null", - "string" - ], - "default": null - } - ] - } + "name": "update_timestamp", + "type": "string" + }, + { + "name": "grading_policy_hash", + "type": "string" } ] } } ], - "namespace": "org.openedx.learning.course.grade.now.failed.v1" + "namespace": "org.openedx.learning.course.passing.status.v1" } \ No newline at end of file diff --git a/openedx_events/learning/data.py b/openedx_events/learning/data.py index d178455e..37938315 100644 --- a/openedx_events/learning/data.py +++ b/openedx_events/learning/data.py @@ -8,6 +8,7 @@ from typing import List, Optional import attr +from attr.validators import in_ from ccx_keys.locator import CCXLocator from opaque_keys.edx.keys import CourseKey, UsageKey @@ -75,6 +76,30 @@ class CourseData: end = attr.ib(type=datetime, default=None) +@attr.s(frozen=True) +class CcxCourseData: + """ + Represents data for a CCX (Custom Courses for edX) course. + + Attributes: + ccx_course_key (CCXLocator): The unique identifier for the CCX course. + master_course_key (CourseKey): The unique identifier for the original course from which the CCX is derived. + display_name (str): The name of the CCX course as it should appear to users. + coach_email (str): The email address of the coach (instructor) for the CCX course. + start (str, optional): The start date of the CCX course. Defaults to None, indicating no specific start date. + end (str, optional): The end date of the CCX course. Defaults to None, indicating no specific end date. + max_students_allowed (int, optional): The maximum number of students that can enroll in the CCX course. Defaults to None, indicating no limit. + """ + + ccx_course_key = attr.ib(type=CCXLocator) + master_course_key = attr.ib(type=CourseKey) + display_name = attr.ib(type=str, factory=str) + coach_email = attr.ib(type=str, factory=str) + start = attr.ib(type=str, default=None) + end = attr.ib(type=str, default=None) + max_students_allowed = attr.ib(type=int, default=None) + + @attr.s(frozen=True) class CourseEnrollmentData: """ @@ -480,17 +505,40 @@ class ORASubmissionData: @attr.s(frozen=True) -class UserCourseData: +class CoursePassingStatusData: """ - Attributes defined for Open edX user course data object. + Represents the event data when a user's grade crosses a grading policy threshold in a course. - Arguments: - user (UserData): user associated with the Course Enrollment. - course (CourseData): course where the user is enrolled in. + Attributes: + user (UserData): An instance of UserData containing information about the user whose grade crossed the threshold. + course (CourseData): An instance of CourseData containing details about the course in which the grade threshold was crossed. + update_timestamp (datetime): The timestamp when the grade crossing event was recorded. + grading_policy_hash (str): A hash of the course's grading policy at the time of the event, used for verifying the grading policy has not changed. """ + PASSING = 'passing' + FAILING = 'failing' + STATUSES = [PASSING, FAILING] - user = attr.ib(type=UserData) + status = attr.ib(type=str, validator=in_(STATUSES)) course = attr.ib(type=CourseData) + user = attr.ib(type=UserData) + update_timestamp = attr.ib(type=datetime) + grading_policy_hash = attr.ib(type=str) + + +@attr.s(frozen=True) +class CcxCoursePassingStatusData(CoursePassingStatusData): + """ + Extends CoursePassingStatusData for CCX courses, specifying CCX course data. + + This class is used for events where a user's grade crosses a threshold specifically in a CCX course, + providing a custom course attribute suited for CCX course instances. + + Attributes: + course (CcxCourseData): An instance of CcxCourseData containing details about the CCX course in which the grade threshold was crossed. + All other attributes are inherited from CoursePassingStatusData. + """ + course = attr.ib(type=CcxCourseData) @attr.s(frozen=True) @@ -527,27 +575,3 @@ class BadgeData: uuid = attr.ib(type=str) user = attr.ib(type=UserData) template = attr.ib(type=BadgeTemplateData) - - -@attr.s(frozen=True) -class CcxCourseData: - """ - Represents data for a CCX (Custom Courses for edX) course. - - Attributes: - ccx_course_key (CCXLocator): The unique identifier for the CCX course. - master_course_key (CourseKey): The unique identifier for the original course from which the CCX is derived. - display_name (str): The name of the CCX course as it should appear to users. - coach_email (str): The email address of the coach (instructor) for the CCX course. - start (str, optional): The start date of the CCX course. Defaults to None, indicating no specific start date. - end (str, optional): The end date of the CCX course. Defaults to None, indicating no specific end date. - max_students_allowed (int, optional): The maximum number of students that can enroll in the CCX course. Defaults to None, indicating no limit. - """ - - ccx_course_key = attr.ib(type=CCXLocator) - master_course_key = attr.ib(type=CourseKey) - display_name = attr.ib(type=str, factory=str) - coach_email = attr.ib(type=str, factory=str) - start = attr.ib(type=str, default=None) - end = attr.ib(type=str, default=None) - max_students_allowed = attr.ib(type=int, default=None) diff --git a/openedx_events/learning/signals.py b/openedx_events/learning/signals.py index 85a7a69e..5064d2d6 100644 --- a/openedx_events/learning/signals.py +++ b/openedx_events/learning/signals.py @@ -10,19 +10,19 @@ from openedx_events.learning.data import ( BadgeData, - CcxCourseData, + CcxCoursePassingStatusData, CertificateData, CohortData, CourseAccessRoleData, CourseDiscussionConfigurationData, CourseEnrollmentData, + CoursePassingStatusData, CourseNotificationData, DiscussionThreadData, ExamAttemptData, ORASubmissionData, PersistentCourseGradeData, ProgramCertificateData, - UserCourseData, UserData, UserNotificationData, XBlockSkillVerificationData, @@ -354,28 +354,30 @@ }, ) -# .. event_type: org.openedx.learning.course.grade.now.passed.v1 -# .. event_name: COURSE_GRADE_NOW_PASSED -# .. event_description: Emmited when course grade is passed. -# .. event_data: UserCourseData -COURSE_GRADE_NOW_PASSED = OpenEdxPublicSignal( - event_type="org.openedx.learning.course.grade.now.passed.v1", +# .. event_type: org.openedx.learning.course.passing.status.v1 +# .. event_name: COURSE_PASSING_STATUS_UPDATED +# .. event_description: Emitted when course grade updates. +# .. event_data: CoursePassingStatusData +COURSE_PASSING_STATUS_UPDATED = OpenEdxPublicSignal( + event_type="org.openedx.learning.course.passing.status.v1", data={ - "user_course_data": UserCourseData, + "course_passing_status": CoursePassingStatusData, } ) -# .. event_type: org.openedx.learning.course.grade.now.failed.v1 -# .. event_name: COURSE_GRADE_NOW_FAILED -# .. event_description: Emmited when course grade is failed. -# .. event_data: UserCourseData -COURSE_GRADE_NOW_FAILED = OpenEdxPublicSignal( - event_type="org.openedx.learning.course.grade.now.failed.v1", + +# .. event_type: org.openedx.learning.ccx.course.passing.status.v1 +# .. event_name: CCX_COURSE_PASSING_STATUS_UPDATED +# .. event_description: Emitted when a CCX course grade updates. +# .. event_data: CcxCoursePassingStatusData +CCX_COURSE_PASSING_STATUS_UPDATED = OpenEdxPublicSignal( + event_type="org.openedx.learning.ccx.course.passing.status.v1", data={ - "user_course_data": UserCourseData, + "course_passing_status": CcxCoursePassingStatusData, } ) + # .. event_type: org.openedx.learning.badge.awarded.v1 # .. event_name: BADGE_AWARDED # .. event_description: Emit when a badge is awarded to a learner @@ -398,27 +400,3 @@ "badge": BadgeData, } ) - - -# .. event_type: org.openedx.learning.ccx.course.grade.now.passed.v1 -# .. event_name: CCX_COURSE_GRADE_NOW_PASSED -# .. event_description: Emit when a ccx course grade is passed -# .. event_data: CcxCourseData -CCX_COURSE_GRADE_NOW_PASSED = OpenEdxPublicSignal( - event_type="org.openedx.learning.ccx.course.grade.now.passed.v1", - data={ - "ccx_course": CcxCourseData, - } -) - - -# .. event_type: org.openedx.learning.ccx.course.grade.now.failed.v1 -# .. event_name: CCX_COURSE_GRADE_NOW_FAILED -# .. event_description: Emit when a ccx course grade is failed -# .. event_data: CcxCourseData -CCX_COURSE_GRADE_NOW_FAILED = OpenEdxPublicSignal( - event_type="org.openedx.learning.ccx.course.grade.now.failed.v1", - data={ - "ccx_course": CcxCourseData, - } -)