diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b833062d..c68c76b3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,12 @@ Changed ~~~~~~~ * Re-licensed this repository from AGPL 3.0 to Apache 2.0 +[8.7.0] - 2023-09-29 +-------------------- +Added +~~~~~ +* Added new ``EXAM_ATTEMPT_SUBMITTED``, ``EXAM_ATTEMPT_REJECTED``, ``EXAM_ATTEMPT_VERIFIED``, ``EXAM_ATTEMPT_RESET``, and ``EXAM_ATTEMPT_ERRORED`` in learning. + [8.6.0] - 2023-08-28 -------------------- Added diff --git a/docs/decisions/0013-special-exam-submission-and-review-events.rst b/docs/decisions/0013-special-exam-submission-and-review-events.rst new file mode 100644 index 00000000..aa6f9ae0 --- /dev/null +++ b/docs/decisions/0013-special-exam-submission-and-review-events.rst @@ -0,0 +1,51 @@ +12. Event definitions for special exam post-submission and review +################################################################# + +Status +****** + +**Draft** 2023-10-02 + +Context +******* + +About Special Exams: +==================== +* Course subsections that have an `exam_type` have additional logic that governs completion, grading, credit requirements, and more based on the `exam_type` value (e.g. timed, proctored, etc). +* These subsections are also known as **Special Exams**. + * NOTE: The events described in this document will only be produced/consumed in the context of **Special Exams**. +* Course subsections that do not have an `exam_type` configured may still have a grading policy named 'Exam'. This type of content does not have the exam user experience and is not governed by any exam specific logic. + +The New Exams IDA: +================== +* A new backend for exams called `edx-exams` is being developed (See the `exams IDA ADR `_ for more info). +* We are currently working to use the event bus to trigger the downstream effects whenever an exam attempt is submitted or reviewed. + * For example, when an exam attempt is submitted, we will want to make sure `edx-platform` knows to mark the exam subsection as completed. + + +Decision +******** + +Where these events will be produced/consumed: +============================================= + +* `edx-exams` will produce these events. + * NOTE: There is no plan to have the legacy exams backend, `edx-proctoring`, produce these events. +* `edx-platform` will consume these events in order to handle all behavior as it pertains to the state of an exam subsection. + +Event Definitions: +================== +* We will define the events that as planned in `the ADR for events in edx-exams `_. + +Note on the Event Data/Signal Names: +==================================== +We are using the prefix "Exam" as opposed to the prefix "Special_Exam" for these events because **Special Exams** will likely be the only type of exam that will be of concern to developers in the context of events for the forseeable future. + + +Consequences +************ + +* `edx-exams` will emit events via the event bus to send information without needing a response. +* Since, `edx-exams` already recieves and responds to REST requests, we will avoid creating circular dependencies because `edx-exams` will not need to send REST requests itself. +* These events are dynamic, in that they can also be consumed by other services/applications as needed in the future. + diff --git a/docs/decisions/index.rst b/docs/decisions/index.rst index 81479709..a73a8b09 100644 --- a/docs/decisions/index.rst +++ b/docs/decisions/index.rst @@ -17,3 +17,4 @@ Architectural Decision Records (ADRs) 0010-multiple-event-types-per-topic 0011-depending-on-multiple-event-bus-implementations 0012-producing-to-event-bus-via-settings + 0013-special-exam-submission-and-review-events diff --git a/openedx_events/__init__.py b/openedx_events/__init__.py index 3feb7fdf..e2ea442e 100644 --- a/openedx_events/__init__.py +++ b/openedx_events/__init__.py @@ -5,4 +5,4 @@ more information about the project. """ -__version__ = "8.6.0" +__version__ = "8.7.0" diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+errored+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+errored+v1_schema.avsc new file mode 100644 index 00000000..eb7c946f --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+errored+v1_schema.avsc @@ -0,0 +1,75 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "exam_attempt", + "type": { + "name": "ExamAttemptData", + "type": "record", + "fields": [ + { + "name": "student_user", + "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": "course_key", + "type": "string" + }, + { + "name": "usage_key", + "type": "string" + }, + { + "name": "exam_type", + "type": "string" + }, + { + "name": "requesting_user", + "type": [ + "null", + "UserData" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.exam.attempt.errored.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+rejected+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+rejected+v1_schema.avsc new file mode 100644 index 00000000..7d726e63 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+rejected+v1_schema.avsc @@ -0,0 +1,75 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "exam_attempt", + "type": { + "name": "ExamAttemptData", + "type": "record", + "fields": [ + { + "name": "student_user", + "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": "course_key", + "type": "string" + }, + { + "name": "usage_key", + "type": "string" + }, + { + "name": "exam_type", + "type": "string" + }, + { + "name": "requesting_user", + "type": [ + "null", + "UserData" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.exam.attempt.rejected.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+reset+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+reset+v1_schema.avsc new file mode 100644 index 00000000..c226aa0a --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+reset+v1_schema.avsc @@ -0,0 +1,75 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "exam_attempt", + "type": { + "name": "ExamAttemptData", + "type": "record", + "fields": [ + { + "name": "student_user", + "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": "course_key", + "type": "string" + }, + { + "name": "usage_key", + "type": "string" + }, + { + "name": "exam_type", + "type": "string" + }, + { + "name": "requesting_user", + "type": [ + "null", + "UserData" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.exam.attempt.reset.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+submitted+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+submitted+v1_schema.avsc new file mode 100644 index 00000000..286747b8 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+submitted+v1_schema.avsc @@ -0,0 +1,75 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "exam_attempt", + "type": { + "name": "ExamAttemptData", + "type": "record", + "fields": [ + { + "name": "student_user", + "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": "course_key", + "type": "string" + }, + { + "name": "usage_key", + "type": "string" + }, + { + "name": "exam_type", + "type": "string" + }, + { + "name": "requesting_user", + "type": [ + "null", + "UserData" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.exam.attempt.submitted.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+verified+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+verified+v1_schema.avsc new file mode 100644 index 00000000..b6250c9c --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+exam+attempt+verified+v1_schema.avsc @@ -0,0 +1,75 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "exam_attempt", + "type": { + "name": "ExamAttemptData", + "type": "record", + "fields": [ + { + "name": "student_user", + "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": "course_key", + "type": "string" + }, + { + "name": "usage_key", + "type": "string" + }, + { + "name": "exam_type", + "type": "string" + }, + { + "name": "requesting_user", + "type": [ + "null", + "UserData" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.exam.attempt.verified.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+user+course+staff+role+added+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+user+course+staff+role+added+v1_schema.avsc new file mode 100644 index 00000000..b683d683 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+user+course+staff+role+added+v1_schema.avsc @@ -0,0 +1,59 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "course_staff_data", + "type": { + "name": "CourseStaffData", + "type": "record", + "fields": [ + { + "name": "course_key", + "type": "string" + }, + { + "name": "user", + "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" + } + ] + } + } + ] + } + } + ] + } + } + ], + "namespace": "org.openedx.learning.user.course.staff.role.added.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+user+course+staff+role+removed+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+user+course+staff+role+removed+v1_schema.avsc new file mode 100644 index 00000000..6b6f9dfa --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+user+course+staff+role+removed+v1_schema.avsc @@ -0,0 +1,59 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "course_staff_data", + "type": { + "name": "CourseStaffData", + "type": "record", + "fields": [ + { + "name": "course_key", + "type": "string" + }, + { + "name": "user", + "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" + } + ] + } + } + ] + } + } + ] + } + } + ], + "namespace": "org.openedx.learning.user.course.staff.role.removed.v1" +} \ No newline at end of file diff --git a/openedx_events/learning/data.py b/openedx_events/learning/data.py index 7e5d2f26..b4189f08 100644 --- a/openedx_events/learning/data.py +++ b/openedx_events/learning/data.py @@ -296,3 +296,36 @@ class ProgramCertificateData: status = attr.ib(type=str) url = attr.ib(type=str) certificate_available_date = attr.ib(type=datetime, default=None) + + +@attr.s(frozen=True) +class ExamAttemptData: + """ + Attributes defined for the Open edX Exam downstream effects. + + Note that events that use this data type: + A. Pretain to "Special Exams", e.g. Timed or Proctored exams, and not non-timed course + subsections that are labelled as an exam. + B. Are only ever emitted from the newer exams backend, edx-exams, and never from the + legacy exams backend, edx-proctoring. + + The event signals that use this data have the prefix `EXAM_`, which is equivalent to "special exam". + We are using this as a shortened form of the prefix `SPECIAL_EXAM` to avoid confusion, as these are likely + the only type of exams that will be of concern in the context of events/the event bus. + + For more information, please see the ADR relating to this event data: + https://github.com/openedx/openedx-events/blob/main/docs/decisions/0013-special-exam-submission-and-review-events.rst + + Arguments: + student_user (UserData): user object for the student to whom the exam attempt belongs + course_key (CourseKey): identifier of the course to which the exam attempt belongs + usage_key (UsageKey): identifier of the content that will get a exam attempt + exam_type (str): type of exam that was taken (e.g. timed, proctored, etc.) + requesting_user (UserData): user triggering the event (sometimes a non-learner, e.g. an instructor) + """ + + student_user = attr.ib(type=UserData) + course_key = attr.ib(type=CourseKey) + usage_key = attr.ib(type=UsageKey) + exam_type = attr.ib(type=str) + requesting_user = attr.ib(type=UserData, default=None) diff --git a/openedx_events/learning/signals.py b/openedx_events/learning/signals.py index 5c139937..ece24cf9 100644 --- a/openedx_events/learning/signals.py +++ b/openedx_events/learning/signals.py @@ -13,6 +13,7 @@ CohortData, CourseDiscussionConfigurationData, CourseEnrollmentData, + ExamAttemptData, PersistentCourseGradeData, ProgramCertificateData, UserData, @@ -196,3 +197,58 @@ "notification_data": UserNotificationData, } ) + +# .. event_type: org.openedx.learning.exam.attempt.submitted.v1 +# .. event_name: EXAM_ATTEMPT_SUBMITTED +# .. event_description: Emitted when an exam attempt is submitted by a learner in edx-exams. +# .. event_data: ExamAttemptData +EXAM_ATTEMPT_SUBMITTED = OpenEdxPublicSignal( + event_type="org.openedx.learning.exam.attempt.submitted.v1", + data={ + "exam_attempt": ExamAttemptData, + } +) + +# .. event_type: org.openedx.learning.exam.attempt.rejected.v1 +# .. event_name: EXAM_ATTEMPT_REJECTED +# .. event_description: Emitted when an exam attempt is marked rejected in edx-exams. +# .. event_data: ExamAttemptData +EXAM_ATTEMPT_REJECTED = OpenEdxPublicSignal( + event_type="org.openedx.learning.exam.attempt.rejected.v1", + data={ + "exam_attempt": ExamAttemptData, + } +) + +# .. event_type: org.openedx.learning.exam.attempt.verified.v1 +# .. event_name: EXAM_ATTEMPT_VERIFIED +# .. event_description: Emitted when an exam attempt is marked verified in edx-exams. +# .. event_data: ExamAttemptData +EXAM_ATTEMPT_VERIFIED = OpenEdxPublicSignal( + event_type="org.openedx.learning.exam.attempt.verified.v1", + data={ + "exam_attempt": ExamAttemptData, + } +) + +# .. event_type: org.openedx.learning.exam.attempt.errored.v1 +# .. event_name: EXAM_ATTEMPT_ERRORED +# .. event_description: Emitted when a learner's exam attempt errors out in edx-exams. +# .. event_data: ExamAttemptData +EXAM_ATTEMPT_ERRORED = OpenEdxPublicSignal( + event_type="org.openedx.learning.exam.attempt.errored.v1", + data={ + "exam_attempt": ExamAttemptData, + } +) + +# .. event_type: org.openedx.learning.exam.attempt.reset.v1 +# .. event_name: EXAM_ATTEMPT_RESET +# .. event_description: Emitted when an exam attempt is reset in edx-exams. +# .. event_data: ExamAttemptData +EXAM_ATTEMPT_RESET = OpenEdxPublicSignal( + event_type="org.openedx.learning.exam.attempt.reset.v1", + data={ + "exam_attempt": ExamAttemptData, + } +)