Skip to content

Commit ee2e241

Browse files
kdmccormickclaude
andcommitted
feat: React to verified upgrades by unarchiving the course
Replaces the placeholder COURSE_CATALOG_INFO_CHANGED handler with one that ties the events example to the rest of the plugin's archive functionality: when a learner's enrollment goes active in the verified mode, any existing CourseArchiveStatus row marking that course as archived is flipped back to unarchived (and the archive_date cleared, matching views.py). Modeled as a one-time event reaction rather than a continuous filter rule so that a learner who deliberately re-archives a verified course after the upgrade has that choice respected. README + apps.py example comment updated to describe the new handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3c39178 commit ee2e241

3 files changed

Lines changed: 88 additions & 130 deletions

File tree

backend-plugin-sample/README.md

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -184,40 +184,62 @@ def perform_create(self, serializer):
184184

185185
### Event Handler Example
186186

187+
This plugin reacts to `COURSE_ENROLLMENT_CHANGED` to unarchive a course on the
188+
learner's dashboard when they upgrade to the verified track. The idea: a learner
189+
who has previously archived a course shouldn't have to dig it back out of their
190+
"Archived" section after upgrading -- their renewed investment is a strong
191+
signal that the course belongs back in their active list.
192+
187193
```python
188-
from openedx_events.content_authoring.signals import COURSE_CATALOG_INFO_CHANGED
194+
from openedx_events.learning.data import CourseEnrollmentData
195+
from openedx_events.learning.signals import COURSE_ENROLLMENT_CHANGED
189196
from django.dispatch import receiver
190197

191-
@receiver(COURSE_CATALOG_INFO_CHANGED)
192-
def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs):
193-
logging.info(f"{catalog_info.course_key} has been updated!")
194-
# Add your custom business logic here
198+
@receiver(COURSE_ENROLLMENT_CHANGED)
199+
def unarchive_on_verified_upgrade(signal, sender, enrollment: CourseEnrollmentData, **kwargs):
200+
if not enrollment.is_active or enrollment.mode != "verified":
201+
return
202+
CourseArchiveStatus.objects.filter(
203+
user_id=enrollment.user.id,
204+
course_id=enrollment.course.course_key,
205+
is_archived=True,
206+
).update(is_archived=False, archive_date=None)
195207
```
196208

209+
**Why an event (not a filter)?** The unarchive is a *one-time nudge*: if the
210+
learner re-archives the course later, we respect that. Implementing this as a
211+
continuous rule in the filter pipeline (e.g. "any verified course is never
212+
archived") would override the learner's intent. Events fire at the moment a
213+
state change happens, which is exactly when this kind of one-shot reaction
214+
belongs.
215+
197216
### Available Events
198217

199218
**Event Catalog**: [Open edX Events Reference](https://docs.openedx.org/projects/openedx-events/en/latest/reference/events.html)
200219

201220
**Common Events:**
202-
- `COURSE_CATALOG_INFO_CHANGED` - Course information updated
221+
- `COURSE_ENROLLMENT_CHANGED` - Enrollment becomes active/inactive or changes mode
222+
- `COURSE_ENROLLMENT_CREATED` - Student newly enrolled in a course
203223
- `STUDENT_REGISTRATION_COMPLETED` - New user registered
204224
- `CERTIFICATE_CREATED` - Certificate generated for learner
205-
- `ENROLLMENT_CREATED` - Student enrolled in course
225+
- `COURSE_CATALOG_INFO_CHANGED` - Course catalog metadata updated
206226

207227
### Event Data Structure
208228

209-
Each event includes specific data. For `COURSE_CATALOG_INFO_CHANGED`:
229+
Each event includes a specific data object. For `COURSE_ENROLLMENT_CHANGED`:
210230

211231
```python
212-
def log_course_info_changed(signal, sender, catalog_info: CourseCatalogData, **kwargs):
213-
# catalog_info contains:
214-
# - course_key: CourseKey object
215-
# - name: Course display name
216-
# - schedule: Course schedule information
217-
# - hidden: Visibility status
232+
def unarchive_on_verified_upgrade(signal, sender, enrollment: CourseEnrollmentData, **kwargs):
233+
# enrollment contains:
234+
# - user: UserData (with .id, .is_active, .pii)
235+
# - course: CourseData (with .course_key, .display_name, .start, .end)
236+
# - mode: str (e.g. "audit", "verified", "honor")
237+
# - is_active: bool
238+
# - creation_date: datetime
239+
# - created_by: UserData (optional)
218240
```
219241

220-
**Key Point**: Check the [event definition](https://docs.openedx.org/projects/openedx-events/en/latest/reference/events.html) to understand what data is available.
242+
**Key Point**: Check the [event data reference](https://docs.openedx.org/projects/openedx-events/en/latest/reference/data.html) to understand the exact fields available for each event.
221243

222244
### Signal Handler Registration
223245

@@ -471,15 +493,19 @@ const response = await client.get(
471493
);
472494
```
473495

474-
### Events + API Integration
496+
### Events + Models Integration
475497

476498
```python
477-
@receiver(COURSE_CATALOG_INFO_CHANGED)
478-
def sync_course_archive_on_change(signal, sender, catalog_info, **kwargs):
479-
# Update archive statuses when course info changes
499+
@receiver(COURSE_ENROLLMENT_CHANGED)
500+
def unarchive_on_verified_upgrade(signal, sender, enrollment, **kwargs):
501+
# React to a verified upgrade by clearing the learner's archive flag
502+
if not enrollment.is_active or enrollment.mode != "verified":
503+
return
480504
CourseArchiveStatus.objects.filter(
481-
course_id=catalog_info.course_key
482-
).update(last_synced=timezone.now())
505+
user_id=enrollment.user.id,
506+
course_id=enrollment.course.course_key,
507+
is_archived=True,
508+
).update(is_archived=False, archive_date=None)
483509
```
484510

485511
### Filters + Settings Integration

backend-plugin-sample/src/openedx_plugin_sample/apps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ class SamplePluginConfig(AppConfig):
9292
# "lms.djangoapp": {
9393
# "relative_path": "signals",
9494
# "receivers": [{
95-
# "receiver_func_name": "log_course_info_changed",
96-
# "signal_path": "openedx_events.content_authoring.signals.COURSE_CATALOG_INFO_CHANGED",
95+
# "receiver_func_name": "unarchive_on_verified_upgrade",
96+
# "signal_path": "openedx_events.learning.signals.COURSE_ENROLLMENT_CHANGED",
9797
# }]
9898
# }
9999
# }
Lines changed: 39 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,64 @@
11
"""
22
Open edX Events signal handlers for the openedx_plugin_sample application.
33
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.
127
138
Key Concepts:
149
- 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
1813
1914
Official Documentation:
2015
- Events Overview: https://docs.openedx.org/projects/openedx-events/en/latest/
2116
- Available Events: https://docs.openedx.org/projects/openedx-events/en/latest/reference/events.html
2217
- 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
4319
"""
4420

4521
import logging
4622

4723
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
5028

5129
logger = logging.getLogger(__name__)
5230

5331

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
5634
"""
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.
7736
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.
8241
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.
8946
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
11049
"""
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

Comments
 (0)