diff --git a/.github/dependabot.yml b/.github/dependabot.yml index df4d15b3..9e401a1b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,4 +5,4 @@ updates: directory: "/" schedule: # Check for updates to GitHub Actions every week - interval: "weekly" + interval: "weekly" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76d6137c..65d1abbb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,8 +11,44 @@ Change Log .. There should always be an "Unreleased" section for changes pending release. + + Unreleased ----------- +__________ + + + +[9.14.0] - 2024-09-12 +--------------------- + +Added +~~~~~ + +* Adds event ``CONTENT_OBJECT_ASSOCIATIONS_CHANGED`` +* Added new ``LIBRARY_COLLECTION_CREATED``, ``LIBRARY_COLLECTION_UPDATED`` and ``LIBRARY_COLLECTION_DELETED`` events in content_authoring. +* Adds ``ContentObjectChangedData``, which inherits from ContentObjectData and adds an optional list of string ``changes``. + +Deprecated +~~~~~~~~~~ + +* Deprecated event ``CONTENT_OBJECT_TAGS_CHANGED`` in favor of ``CONTENT_OBJECT_ASSOCIATIONS_CHANGED`` + Plan to remove after Sumac. + +[9.13.0] - 2024-09-05 +--------------------- + +Added +~~~~~~~ + +* Added new IDV events ``LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED``, ``IDV_ATTEMPT_CREATED``, ``IDV_ATTEMPT_PENDING``, ``IDV_ATTEMPT_APPROVED``, and ``IDV_ATTEMPT_DENIED`` in learning. + +[9.12.0] - 2024-07-31 +--------------------- + +Added +~~~~~~~ + +* Added new enterprise signal ``LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED``. [9.11.0] - 2024-05-15 --------------------- diff --git a/docs/concepts/hooks-extension-framework.rst b/docs/concepts/hooks-extension-framework.rst index 1ad61964..d298f319 100644 --- a/docs/concepts/hooks-extension-framework.rst +++ b/docs/concepts/hooks-extension-framework.rst @@ -16,7 +16,7 @@ are meant to be extended using Open edX plugins and configuration. Hooks can be of two types, events and filters. Events are in essence signals, in that they are sent in specific application places and whose listeners can extend -functionality. functionality. On the other hand Filters can be used to act on data before +functionality. On the other hand Filters can be used to act on data before it is put back in the original application flow. In order to allow extension developers to use the Events and Filters definitions on their plugins, both kinds of hooks are defined in lightweight external libraries. diff --git a/openedx_events/__init__.py b/openedx_events/__init__.py index 99ce4e4a..40b95950 100644 --- a/openedx_events/__init__.py +++ b/openedx_events/__init__.py @@ -5,4 +5,4 @@ more information about the project. """ -__version__ = "9.11.0" +__version__ = "9.14.0" diff --git a/openedx_events/content_authoring/data.py b/openedx_events/content_authoring/data.py index 1483ddbc..4e76bcd7 100644 --- a/openedx_events/content_authoring/data.py +++ b/openedx_events/content_authoring/data.py @@ -185,7 +185,7 @@ class LibraryBlockData: @attr.s(frozen=True) class ContentObjectData: """ - Data about changed content object. + Data about a content object. Arguments: object_id (str): identifier of the Content object. This represents the id of the course or library block @@ -194,3 +194,34 @@ class ContentObjectData: """ object_id = attr.ib(type=str) + + +@attr.s(frozen=True) +class ContentObjectChangedData(ContentObjectData): + """ + Data about a changed content object. + + Arguments: + object_id (str): identifier of the Content object. This represents the id of the course or library block + as a string. For example: + block-v1:SampleTaxonomyOrg2+STC1+2023_1+type@vertical+block@f8de78f0897049ce997777a3a31b6ea0 + + changes: list of changes made to this ContentObject, e.g. "tags", "collections" + If list is empty, assume everything has changed. + """ + + changes = attr.ib(type=List[str], factory=list) + + +@attr.s(frozen=True) +class LibraryCollectionData: + """ + Data about changed content library Collection. + + Arguments: + library_key (LibraryLocatorV2): a key that represents a Blockstore-based content library. + collection_key (str): identifies the collection within the library's learning package + """ + + library_key = attr.ib(type=LibraryLocatorV2) + collection_key = attr.ib(type=str) diff --git a/openedx_events/content_authoring/signals.py b/openedx_events/content_authoring/signals.py index 495d14d9..dc323827 100644 --- a/openedx_events/content_authoring/signals.py +++ b/openedx_events/content_authoring/signals.py @@ -10,11 +10,13 @@ from openedx_events.content_authoring.data import ( CertificateConfigData, ContentLibraryData, + ContentObjectChangedData, ContentObjectData, CourseCatalogData, CourseData, DuplicatedXBlockData, LibraryBlockData, + LibraryCollectionData, XBlockData, ) from openedx_events.tooling import OpenEdxPublicSignal @@ -200,9 +202,21 @@ } ) +# .. event_type: org.openedx.content_authoring.content.object.associations.changed.v1 +# .. event_name: CONTENT_OBJECT_ASSOCIATIONS_CHANGED +# .. event_description: emitted when an object's associations are changed, e.g tags, collections +# .. event_data: ContentObjectData +CONTENT_OBJECT_ASSOCIATIONS_CHANGED = OpenEdxPublicSignal( + event_type="org.openedx.content_authoring.content.object.associations.changed.v1", + data={ + "content_object": ContentObjectChangedData + } +) + # .. event_type: org.openedx.content_authoring.content.object.tags.changed.v1 # .. event_name: CONTENT_OBJECT_TAGS_CHANGED # .. event_description: emitted when an object's tags are changed +# DEPRECATED: please use CONTENT_OBJECT_ASSOCIATIONS_CHANGED instead. # .. event_data: ContentObjectData CONTENT_OBJECT_TAGS_CHANGED = OpenEdxPublicSignal( event_type="org.openedx.content_authoring.content.object.tags.changed.v1", @@ -210,3 +224,36 @@ "content_object": ContentObjectData } ) + +# .. event_type: org.openedx.content_authoring.content_library.collection.created.v1 +# .. event_name: LIBRARY_COLLECTION_CREATED +# .. event_description: emitted when a content library collection is created +# .. event_data: LibraryCollectionData +LIBRARY_COLLECTION_CREATED = OpenEdxPublicSignal( + event_type="org.openedx.content_authoring.content_library.collection.created.v1", + data={ + "library_collection": LibraryCollectionData + } +) + +# .. event_type: org.openedx.content_authoring.content_library.collection.updated.v1 +# .. event_name: LIBRARY_COLLECTION_UPDATED +# .. event_description: emitted when when a content library collection is updated +# .. event_data: LibraryCollectionData +LIBRARY_COLLECTION_UPDATED = OpenEdxPublicSignal( + event_type="org.openedx.content_authoring.content_library.collection.updated.v1", + data={ + "library_collection": LibraryCollectionData + } +) + +# .. event_type: org.openedx.content_authoring.content_library.collection.deleted.v1 +# .. event_name: LIBRARY_COLLECTION_DELETED +# .. event_description: emitted when an when a content library collection is deleted +# .. event_data: LibraryCollectionData +LIBRARY_COLLECTION_DELETED = OpenEdxPublicSignal( + event_type="org.openedx.content_authoring.content_library.collection.deleted.v1", + data={ + "library_collection": LibraryCollectionData + } +) diff --git a/openedx_events/enterprise/data.py b/openedx_events/enterprise/data.py index c3b3d1f6..7e9283b6 100644 --- a/openedx_events/enterprise/data.py +++ b/openedx_events/enterprise/data.py @@ -94,3 +94,122 @@ class LedgerTransaction(BaseLedgerTransaction): parent_content_key = attr.ib(type=str, default=None) fulfillment_identifier = attr.ib(type=str, default=None) reversal = attr.ib(type=LedgerTransactionReversal, default=None) + + +@attr.s(frozen=True) +class EnterpriseCustomerUser: + """ + Attributes of an ``enterprise.EnterpriseCustomerUser`` record. + + Django model definition: https://github.com/openedx/edx-enterprise/blob/cc873d6/enterprise/models.py#L1036 + + Arguments: + id (int): Primary identifier of the record. + created (datetime): When the record was created. + modified (datetime): When the record was last modified. + enterprise_customer_uuid (UUID): The enterprise customer to which the user is linked. + user_id (int): The LMS user ID corresponding to this enterprise user. + active (bool): The active enterprise user for the given LMS user. + linked (bool): This enterprise user has been linked to an enterprise customer. + is_relinkable (bool): When set to False, the user cannot be relinked to the enterprise. + invite_key (UUID): Invite key used to link a learner to an enterprise. + should_inactivate_other_customers (bool): When enabled along with `active`, all other linked enterprise + customers for this user will be marked as inactive upon save. + """ + + id = attr.ib(type=int) + created = attr.ib(type=datetime) + modified = attr.ib(type=datetime) + enterprise_customer_uuid = attr.ib(type=UUID) + user_id = attr.ib(type=int) + active = attr.ib(type=bool) + linked = attr.ib(type=bool) + is_relinkable = attr.ib(type=bool) + invite_key = attr.ib(type=UUID) + should_inactivate_other_customers = attr.ib(type=bool) + + +@attr.s(frozen=True) +class EnterpriseCourseEnrollment: + """ + Attributes of an ``enterprise.EnterpriseCourseEnrollment`` record. + + Django model definition: https://github.com/openedx/edx-enterprise/blob/cc873d6/enterprise/models.py#L1983 + + Arguments: + id (int): Primary identifier of the record. + created (datetime): When the record was created. + modified (datetime): When the record was last modified. + enterprise_customer_user (EnterpriseCustomerUser): The enterprise learner to which this enrollment is attached. + course_id (CourseKey): The ID of the course in which the learner was enrolled. + saved_for_later (bool): Specifies whether a user marked this course as saved for later in the learner portal. + source_slug (str): DB slug for the source of the enrollment, e.g. "enrollment_task", "management_command", etc. + unenrolled (bool): Specifies whether the related LMS course enrollment object was unenrolled. + unenrolled_at (datetime): Specifies when the related LMS course enrollment object was unenrolled. + """ + + id = attr.ib(type=int) + created = attr.ib(type=datetime) + modified = attr.ib(type=datetime) + enterprise_customer_user = attr.ib(type=EnterpriseCustomerUser) + course_id = attr.ib(type=CourseKey) + saved_for_later = attr.ib(type=bool) + source_slug = attr.ib(type=str) + unenrolled = attr.ib(type=bool) + unenrolled_at = attr.ib(type=datetime) + + +@attr.s(frozen=True) +class BaseEnterpriseFulfillment: + """ + Defines the common attributes of enterprise fulfillment classes, i.e. ``enterprise.EnterpriseFulfillmentSource``. + + Django model definition: https://github.com/openedx/edx-enterprise/blob/cc873d6/enterprise/models.py#L2213 + + Arguments: + uuid (str): Primary identifier of the record. + created (datetime): When the record was created. + modified (datetime): When the record was last modified. + fulfillment_type (str): Subsidy fulfillment type, typical values: "license", "learner_credit", "coupon_code". + enterprise_course_entitlement_uuid (UUID): The course entitlement the associated subsidy is for. + enterprise_course_enrollment (EnterpriseCourseEnrollment): The course enrollment the associated subsidy is for. + is_revoked (bool): Whether the enterprise subsidy is revoked, e.g., when a user's license is revoked. + """ + + uuid = attr.ib(type=UUID) + created = attr.ib(type=datetime) + modified = attr.ib(type=datetime) + fulfillment_type = attr.ib(type=str) + enterprise_course_entitlement_uuid = attr.ib(type=UUID) + enterprise_course_enrollment = attr.ib(type=EnterpriseCourseEnrollment) + is_revoked = attr.ib(type=bool) + + +@attr.s(frozen=True) +class LearnerCreditEnterpriseCourseEnrollment(BaseEnterpriseFulfillment): + """ + Attributes of an ``enterprise.LearnerCreditEnterpriseCourseEnrollment`` record. + + Django model definition: https://github.com/openedx/edx-enterprise/blob/cc873d6/enterprise/models.py#L2325 + + Arguments: + (All of the same from BaseEnterpriseFulfillment plus the following:) + transaction_id (UUID): Ledgered transaction UUID to associate with this learner credit fulfillment. + """ + + transaction_id = attr.ib(type=UUID) + + +@attr.s(frozen=True) +class LicensedEnterpriseCourseEnrollment(BaseEnterpriseFulfillment): + """ + Attributes of an ``enterprise.LicensedEnterpriseCourseEnrollment`` record. + + Django model definition: https://github.com/openedx/edx-enterprise/blob/cc873d6/enterprise/models.py#L2355 + + Arguments: + (All of the same from BaseEnterpriseFulfillment plus the following:) + license_uuid (UUID): License UUID to associate with this enterprise license fulfillment. + """ + + license_uuid = attr.ib(type=UUID) diff --git a/openedx_events/enterprise/signals.py b/openedx_events/enterprise/signals.py index 50c8a3b7..ee16b910 100644 --- a/openedx_events/enterprise/signals.py +++ b/openedx_events/enterprise/signals.py @@ -8,7 +8,7 @@ docs/decisions/0003-events-payload.rst """ -from openedx_events.enterprise.data import LedgerTransaction, SubsidyRedemption +from openedx_events.enterprise.data import LearnerCreditEnterpriseCourseEnrollment, LedgerTransaction, SubsidyRedemption from openedx_events.tooling import OpenEdxPublicSignal # .. event_type: org.openedx.enterprise.subsidy.redeemed.v1 @@ -84,3 +84,16 @@ "ledger_transaction": LedgerTransaction, } ) + + +# .. event_type: org.openedx.enterprise.learner_credit_course_enrollment.revoked.v1 +# .. event_name: LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED +# .. event_description: emitted when a LearnerCreditEnterpriseCourseEnrollment is revoked. This most often happens when +# an enterprise learner unenrolls from a course which was LC-subsidized. +# .. event_data: LearnerCreditEnterpriseCourseEnrollment +LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED = OpenEdxPublicSignal( + event_type="org.openedx.enterprise.learner_credit_course_enrollment.revoked.v1", + data={ + "learner_credit_course_enrollment": LearnerCreditEnterpriseCourseEnrollment, + } +) diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content+object+associations+changed+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content+object+associations+changed+v1_schema.avsc new file mode 100644 index 00000000..5f3cfb2c --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content+object+associations+changed+v1_schema.avsc @@ -0,0 +1,28 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "content_object", + "type": { + "name": "ContentObjectChangedData", + "type": "record", + "fields": [ + { + "name": "object_id", + "type": "string" + }, + { + "name": "changes", + "type": { + "type": "array", + "items": "string" + } + } + ] + } + } + ], + "namespace": "org.openedx.content_authoring.content.object.associations.changed.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+created+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+created+v1_schema.avsc new file mode 100644 index 00000000..58b77a50 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+created+v1_schema.avsc @@ -0,0 +1,25 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "library_collection", + "type": { + "name": "LibraryCollectionData", + "type": "record", + "fields": [ + { + "name": "library_key", + "type": "string" + }, + { + "name": "collection_key", + "type": "string" + } + ] + } + } + ], + "namespace": "org.openedx.content_authoring.content_library.collection.created.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+deleted+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+deleted+v1_schema.avsc new file mode 100644 index 00000000..473d4163 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+deleted+v1_schema.avsc @@ -0,0 +1,25 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "library_collection", + "type": { + "name": "LibraryCollectionData", + "type": "record", + "fields": [ + { + "name": "library_key", + "type": "string" + }, + { + "name": "collection_key", + "type": "string" + } + ] + } + } + ], + "namespace": "org.openedx.content_authoring.content_library.collection.deleted.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+updated+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+updated+v1_schema.avsc new file mode 100644 index 00000000..54733048 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+content_authoring+content_library+collection+updated+v1_schema.avsc @@ -0,0 +1,25 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "library_collection", + "type": { + "name": "LibraryCollectionData", + "type": "record", + "fields": [ + { + "name": "library_key", + "type": "string" + }, + { + "name": "collection_key", + "type": "string" + } + ] + } + } + ], + "namespace": "org.openedx.content_authoring.content_library.collection.updated.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+enterprise+learner_credit_course_enrollment+revoked+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+enterprise+learner_credit_course_enrollment+revoked+v1_schema.avsc new file mode 100644 index 00000000..75fb19ef --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+enterprise+learner_credit_course_enrollment+revoked+v1_schema.avsc @@ -0,0 +1,135 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "learner_credit_course_enrollment", + "type": { + "name": "LearnerCreditEnterpriseCourseEnrollment", + "type": "record", + "fields": [ + { + "name": "uuid", + "type": "string" + }, + { + "name": "created", + "type": "string" + }, + { + "name": "modified", + "type": "string" + }, + { + "name": "fulfillment_type", + "type": "string" + }, + { + "name": "enterprise_course_entitlement_uuid", + "type": "string" + }, + { + "name": "enterprise_course_enrollment", + "type": { + "name": "EnterpriseCourseEnrollment", + "type": "record", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "created", + "type": "string" + }, + { + "name": "modified", + "type": "string" + }, + { + "name": "enterprise_customer_user", + "type": { + "name": "EnterpriseCustomerUser", + "type": "record", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "created", + "type": "string" + }, + { + "name": "modified", + "type": "string" + }, + { + "name": "enterprise_customer_uuid", + "type": "string" + }, + { + "name": "user_id", + "type": "long" + }, + { + "name": "active", + "type": "boolean" + }, + { + "name": "linked", + "type": "boolean" + }, + { + "name": "is_relinkable", + "type": "boolean" + }, + { + "name": "invite_key", + "type": "string" + }, + { + "name": "should_inactivate_other_customers", + "type": "boolean" + } + ] + } + }, + { + "name": "course_id", + "type": "string" + }, + { + "name": "saved_for_later", + "type": "boolean" + }, + { + "name": "source_slug", + "type": "string" + }, + { + "name": "unenrolled", + "type": "boolean" + }, + { + "name": "unenrolled_at", + "type": "string" + } + ] + } + }, + { + "name": "is_revoked", + "type": "boolean" + }, + { + "name": "transaction_id", + "type": "string" + } + ] + } + } + ], + "namespace": "org.openedx.enterprise.learner_credit_course_enrollment.revoked.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+approved+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+approved+v1_schema.avsc new file mode 100644 index 00000000..e6a186ae --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+approved+v1_schema.avsc @@ -0,0 +1,79 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "idv_attempt", + "type": { + "name": "VerificationAttemptData", + "type": "record", + "fields": [ + { + "name": "attempt_id", + "type": "long" + }, + { + "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" + } + ] + } + } + ] + } + }, + { + "name": "status", + "type": "string" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "expiration_date", + "type": [ + "null", + "string" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.idv_attempt.approved.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+created+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+created+v1_schema.avsc new file mode 100644 index 00000000..665f18ad --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+created+v1_schema.avsc @@ -0,0 +1,79 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "idv_attempt", + "type": { + "name": "VerificationAttemptData", + "type": "record", + "fields": [ + { + "name": "attempt_id", + "type": "long" + }, + { + "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" + } + ] + } + } + ] + } + }, + { + "name": "status", + "type": "string" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "expiration_date", + "type": [ + "null", + "string" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.idv_attempt.created.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+denied+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+denied+v1_schema.avsc new file mode 100644 index 00000000..4059bcf4 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+denied+v1_schema.avsc @@ -0,0 +1,79 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "idv_attempt", + "type": { + "name": "VerificationAttemptData", + "type": "record", + "fields": [ + { + "name": "attempt_id", + "type": "long" + }, + { + "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" + } + ] + } + } + ] + } + }, + { + "name": "status", + "type": "string" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "expiration_date", + "type": [ + "null", + "string" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.idv_attempt.denied.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+pending+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+pending+v1_schema.avsc new file mode 100644 index 00000000..6e3d639b --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+idv_attempt+pending+v1_schema.avsc @@ -0,0 +1,79 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "idv_attempt", + "type": { + "name": "VerificationAttemptData", + "type": "record", + "fields": [ + { + "name": "attempt_id", + "type": "long" + }, + { + "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" + } + ] + } + } + ] + } + }, + { + "name": "status", + "type": "string" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "expiration_date", + "type": [ + "null", + "string" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.learning.idv_attempt.pending.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/test_avro.py b/openedx_events/event_bus/avro/tests/test_avro.py index 4d564b27..87634067 100644 --- a/openedx_events/event_bus/avro/tests/test_avro.py +++ b/openedx_events/event_bus/avro/tests/test_avro.py @@ -108,6 +108,7 @@ def generate_test_event_data_for_data_type(data_type): # pragma: no cover LibraryLocatorV2: LibraryLocatorV2.from_string('lib:MITx:reallyhardproblems'), LibraryUsageLocatorV2: LibraryUsageLocatorV2.from_string('lb:MITx:reallyhardproblems:problem:problem1'), List[int]: [1, 2, 3], + List[str]: ["hi", "there"], datetime: datetime.now(), CCXLocator: CCXLocator(org='edx', course='DemoX', run='Demo_course', ccx='1'), UUID: uuid4(), diff --git a/openedx_events/learning/data.py b/openedx_events/learning/data.py index ba22df9a..962dda35 100644 --- a/openedx_events/learning/data.py +++ b/openedx_events/learning/data.py @@ -589,3 +589,23 @@ class BadgeData: uuid = attr.ib(type=str) user = attr.ib(type=UserData) template = attr.ib(type=BadgeTemplateData) + + +@attr.s(frozen=True) +class VerificationAttemptData: + """ + Attributes defined for the Open edX IDV attempt data object. + + Arguments: + attempt_id (int): the id of the verification attempt + user (User): the user (usually a learner) performing the verification attempt. + status (string): the status of the verification attempt. + name (string): the name being ID verified. Defaults to None. + expiration_datetime (datetime, optional): When the verification attempt expires. Defaults to None. + """ + + attempt_id = attr.ib(type=int) + user = attr.ib(type=UserData) + status = attr.ib(type=str) + name = attr.ib(type=str, default=None) + expiration_date = attr.ib(type=datetime, default=None) diff --git a/openedx_events/learning/signals.py b/openedx_events/learning/signals.py index d3970066..68fd2118 100644 --- a/openedx_events/learning/signals.py +++ b/openedx_events/learning/signals.py @@ -25,6 +25,7 @@ ProgramCertificateData, UserData, UserNotificationData, + VerificationAttemptData, XBlockSkillVerificationData, ) from openedx_events.tooling import OpenEdxPublicSignal @@ -401,3 +402,51 @@ "badge": BadgeData, } ) + + +# .. event_type: org.openedx.learning.idv_attempt.created.v1 +# .. event_name: IDV_ATTEMPT_CREATED +# .. event_description: Emitted when an IDV attempt is created +# .. event_data: VerificationAttemptData +IDV_ATTEMPT_CREATED = OpenEdxPublicSignal( + event_type="org.openedx.learning.idv_attempt.created.v1", + data={ + "idv_attempt": VerificationAttemptData, + } +) + + +# .. event_type: org.openedx.learning.idv_attempt.pending.v1 +# .. event_name: IDV_ATTEMPT_PENDING +# .. event_description: Emitted when an IDV attempt is marked as pending +# .. event_data: VerificationAttemptData +IDV_ATTEMPT_PENDING = OpenEdxPublicSignal( + event_type="org.openedx.learning.idv_attempt.pending.v1", + data={ + "idv_attempt": VerificationAttemptData, + } +) + + +# .. event_type: org.openedx.learning.idv_attempt.approved.v1 +# .. event_name: IDV_ATTEMPT_APPROVED +# .. event_description: Emitted when an IDV attempt is approved +# .. event_data: VerificationAttemptData +IDV_ATTEMPT_APPROVED = OpenEdxPublicSignal( + event_type="org.openedx.learning.idv_attempt.approved.v1", + data={ + "idv_attempt": VerificationAttemptData, + } +) + + +# .. event_type: org.openedx.learning.idv_attempt.denied.v1 +# .. event_name: IDV_ATTEMPT_DENIED +# .. event_description: Emitted when an IDV attempt is denied +# .. event_data: VerificationAttemptData +IDV_ATTEMPT_DENIED = OpenEdxPublicSignal( + event_type="org.openedx.learning.idv_attempt.denied.v1", + data={ + "idv_attempt": VerificationAttemptData, + } +)