|
1 | 1 | """ |
2 | 2 | Open edX Events signal handlers for the openedx_plugin_sample application. |
3 | 3 |
|
4 | | -This module demonstrates how to consume Open edX Events (signals) to react to |
5 | | -platform activities and integrate with external systems. Events are part of |
6 | | -the Hooks Extension Framework and provide a stable way to extend Open edX. |
7 | | -
|
8 | | -What Are Open edX Events? |
9 | | -Events are signals sent when specific actions occur in the platform. Unlike |
10 | | -traditional Django signals, Open edX Events have standardized data structures |
11 | | -and are designed for external consumption. |
| 4 | +This module demonstrates how to consume Open edX Events to react to platform |
| 5 | +activity. Events are part of the Hooks Extension Framework and provide a |
| 6 | +stable way to extend Open edX without modifying core code. |
12 | 7 |
|
13 | 8 | Key Concepts: |
14 | 9 | - Events are fired at specific points in the platform lifecycle |
15 | | -- Each event includes structured data (defined in openedx-events) |
16 | | -- Event handlers can perform actions but cannot modify the event data |
17 | | -- Events support both internal processing and external event bus integration |
| 10 | +- Each event delivers a structured data object (defined in openedx-events) |
| 11 | +- Event handlers can take action but cannot modify the event payload |
| 12 | +- Handlers must be imported from apps.py ready() so @receiver registers them |
18 | 13 |
|
19 | 14 | Official Documentation: |
20 | 15 | - Events Overview: https://docs.openedx.org/projects/openedx-events/en/latest/ |
21 | 16 | - Available Events: https://docs.openedx.org/projects/openedx-events/en/latest/reference/events.html |
22 | 17 | - Consuming Events: https://docs.openedx.org/projects/openedx-events/en/latest/how-tos/consume-an-event.html |
23 | | -- Hooks Framework: https://docs.openedx.org/en/latest/developers/concepts/hooks_extension_framework.html |
24 | | -
|
25 | | -Registration Process: |
26 | | -1. Import the event signal from openedx-events |
27 | | -2. Create handler function with correct signature |
28 | | -3. Decorate with @receiver |
29 | | -4. Import this module in apps.py ready() method |
30 | | -
|
31 | | -Event Data Structure: |
32 | | -Each event defines specific data attributes. Check the event definition in the |
33 | | -official documentation to understand available data: |
34 | | -- Signal Reference: https://docs.openedx.org/projects/openedx-events/en/latest/reference/events.html |
35 | | -- Data Objects: https://docs.openedx.org/projects/openedx-events/en/latest/reference/data.html |
36 | | -- Example: COURSE_CATALOG_INFO_CHANGED provides catalog_info: CourseCatalogData |
37 | | -
|
38 | | -Common Use Cases: |
39 | | -- Integration with external systems (CRM, analytics, notifications) |
40 | | -- Custom logging and audit trails |
41 | | -- Triggering workflows in other services |
42 | | -- Synchronizing data with external databases |
| 18 | +- Event Data Objects: https://docs.openedx.org/projects/openedx-events/en/latest/reference/data.html |
43 | 19 | """ |
44 | 20 |
|
45 | 21 | import logging |
46 | 22 |
|
47 | 23 | from django.dispatch import receiver |
48 | | -from openedx_events.content_authoring.data import CourseCatalogData |
49 | | -from openedx_events.content_authoring.signals import COURSE_CATALOG_INFO_CHANGED |
| 24 | +from openedx_events.learning.data import CourseEnrollmentData |
| 25 | +from openedx_events.learning.signals import COURSE_ENROLLMENT_CHANGED |
| 26 | + |
| 27 | +from .models import CourseArchiveStatus |
50 | 28 |
|
51 | 29 | logger = logging.getLogger(__name__) |
52 | 30 |
|
53 | 31 |
|
54 | | -@receiver(COURSE_CATALOG_INFO_CHANGED) |
55 | | -def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs): # pylint: disable=unused-argument # noqa: E501 |
| 32 | +@receiver(COURSE_ENROLLMENT_CHANGED) |
| 33 | +def unarchive_on_verified_upgrade(signal, sender, enrollment: CourseEnrollmentData, **kwargs): # pylint: disable=unused-argument |
56 | 34 | """ |
57 | | - Handle course catalog information changes. |
58 | | -
|
59 | | - This function demonstrates how to consume the COURSE_CATALOG_INFO_CHANGED event, |
60 | | - which is fired whenever course catalog information is updated in the platform. |
61 | | -
|
62 | | - Event Trigger Conditions: |
63 | | - - Course metadata is modified (name, description, etc.) |
64 | | - - Course schedule is updated |
65 | | - - Course visibility settings change |
66 | | - - Other catalog-related modifications |
67 | | -
|
68 | | - Args: |
69 | | - signal: The signal instance that triggered this handler |
70 | | - sender: The model class that sent the signal |
71 | | - catalog_info (CourseCatalogData): Structured data about the course |
72 | | - **kwargs: Additional context parameters |
73 | | -
|
74 | | - CourseCatalogData Attributes: |
75 | | - Based on the official data structure documentation: |
76 | | - https://docs.openedx.org/projects/openedx-events/en/latest/reference/data.html#openedx_events.content_authoring.data.CourseCatalogData |
| 35 | + Unarchive a course on the learner's dashboard when they upgrade to verified. |
77 | 36 |
|
78 | | - - course_key (CourseKey): Unique course identifier |
79 | | - - name (str): Course display name |
80 | | - - schedule (CourseScheduleData): Start/end dates and pacing |
81 | | - - hidden (bool): Course visibility status |
| 37 | + If a learner has previously archived a course (CourseArchiveStatus.is_archived=True) |
| 38 | + and then upgrades to the verified track, the course shouldn't stay tucked away |
| 39 | + in their "Archived" section -- their renewed investment in the course is a |
| 40 | + strong signal that they want it back in their active list. |
82 | 41 |
|
83 | | - Real-World Use Cases: |
84 | | - - Sync course metadata with external systems (CRM, marketing sites) |
85 | | - - Update search indexes when course information changes |
86 | | - - Trigger email notifications to administrators |
87 | | - - Log changes for audit and compliance |
88 | | - - Update analytics dashboards with new course information |
| 42 | + This is intentionally a one-time nudge, not a continuous rule: if the learner |
| 43 | + re-archives the course later, we respect that choice. That's why we react to |
| 44 | + the enrollment-change *event* rather than computing `isArchivedByLearner` |
| 45 | + from enrollment mode in the filter pipeline. |
89 | 46 |
|
90 | | - Example Implementation:: |
91 | | -
|
92 | | - # Send to external CRM system |
93 | | - external_api.update_course( |
94 | | - course_id=str(catalog_info.course_key), |
95 | | - name=catalog_info.name, |
96 | | - is_hidden=catalog_info.hidden |
97 | | - ) |
98 | | -
|
99 | | - # Update internal tracking |
100 | | - CourseChangeLog.objects.create( |
101 | | - course_key=catalog_info.course_key, |
102 | | - change_type='catalog_updated', |
103 | | - timestamp=timezone.now() |
104 | | - ) |
105 | | -
|
106 | | - Performance Considerations: |
107 | | - - Keep processing lightweight (events should not block platform operations) |
108 | | - - Use asynchronous tasks for heavy processing (Celery, etc.) |
109 | | - - Handle exceptions gracefully to prevent platform disruption |
| 47 | + Event reference: |
| 48 | + https://docs.openedx.org/projects/openedx-events/en/latest/reference/events.html#openedx_events.learning.signals.COURSE_ENROLLMENT_CHANGED |
110 | 49 | """ |
111 | | - logging.info(f"Course catalog updated: {catalog_info.course_key}") |
112 | | - |
113 | | - # Access available data from the event |
114 | | - logging.debug(f"Course name: {catalog_info.name}") |
115 | | - logging.debug(f"Course hidden: {catalog_info.hidden}") |
116 | | - |
117 | | - # Example: Integrate with external systems |
118 | | - # try: |
119 | | - # # Send to external system |
120 | | - # external_system.notify_course_update( |
121 | | - # course_id=str(catalog_info.course_key), |
122 | | - # course_name=catalog_info.name, |
123 | | - # is_hidden=catalog_info.hidden |
124 | | - # ) |
125 | | - # except Exception as e: |
126 | | - # logging.error(f"Failed to notify external system: {e}") |
127 | | - |
128 | | - # Example: Update internal tracking |
129 | | - # from .models import CourseArchiveStatus |
130 | | - # CourseArchiveStatus.objects.filter( |
131 | | - # course_id=catalog_info.course_key |
132 | | - # ).update(last_catalog_update=timezone.now()) |
| 50 | + if not enrollment.is_active or enrollment.mode != "verified": |
| 51 | + return |
| 52 | + |
| 53 | + updated = CourseArchiveStatus.objects.filter( |
| 54 | + user_id=enrollment.user.id, |
| 55 | + course_id=enrollment.course.course_key, |
| 56 | + is_archived=True, |
| 57 | + ).update(is_archived=False, archive_date=None) |
| 58 | + |
| 59 | + if updated: |
| 60 | + logger.info( |
| 61 | + "Unarchived course %s for user %s after verified upgrade", |
| 62 | + enrollment.course.course_key, |
| 63 | + enrollment.user.id, |
| 64 | + ) |
0 commit comments