Skip to content

Commit 87a1c4f

Browse files
committed
feat(enrollments): surface audit expiration date
1 parent 174d639 commit 87a1c4f

3 files changed

Lines changed: 207 additions & 32 deletions

File tree

openedx/core/djangoapps/enrollments/serializers.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def get_pacing_type(self, obj):
6969

7070

7171
class CourseEnrollmentSerializer(serializers.ModelSerializer):
72-
"""Serializes CourseEnrollment models
72+
"""Serializes CourseEnrollment models.
7373
7474
Aggregates all data from the Course Enrollment table, and pulls in the serialization for
7575
the Course block and course modes, to give a complete representation of course enrollment.
@@ -78,14 +78,62 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
7878

7979
course_details = CourseSerializer(source="course_overview")
8080
user = serializers.SerializerMethodField("get_username")
81+
accessExpirationDate = serializers.SerializerMethodField()
82+
isAudit = serializers.SerializerMethodField()
8183

8284
def get_username(self, model):
8385
"""Retrieves the username from the associated model."""
8486
return model.username
8587

88+
def _get_verified_mode(self, course_id):
89+
"""
90+
Cache verified mode lookup to avoid repeated DB queries.
91+
"""
92+
if not hasattr(self, '_verified_mode_cache'):
93+
self._verified_mode_cache = {}
94+
95+
if course_id not in self._verified_mode_cache:
96+
self._verified_mode_cache[course_id] = CourseMode.objects.filter(
97+
course_id=course_id,
98+
mode_slug='verified'
99+
).order_by('_expiration_datetime').first()
100+
101+
return self._verified_mode_cache[course_id]
102+
103+
def get_accessExpirationDate(self, obj):
104+
"""
105+
Returns expiration date for audit enrollments based on verified mode.
106+
"""
107+
if obj.mode != CourseMode.AUDIT:
108+
return None
109+
110+
verified_mode = self._get_verified_mode(obj.course_id)
111+
112+
if verified_mode and verified_mode.expiration_datetime:
113+
return verified_mode.expiration_datetime.isoformat().replace('+00:00', 'Z')
114+
115+
return None
116+
117+
def get_isAudit(self, obj):
118+
"""
119+
Returns True if:
120+
- enrollment is in audit mode
121+
- AND verified mode has expiration
122+
123+
NOTE:
124+
This does not strictly mean "audit mode only".
125+
It reflects audit mode with an expiring upgrade window.
126+
"""
127+
if obj.mode != CourseMode.AUDIT:
128+
return False
129+
130+
verified_mode = self._get_verified_mode(obj.course_id)
131+
132+
return bool(verified_mode and verified_mode.expiration_datetime)
133+
86134
class Meta:
87135
model = CourseEnrollment
88-
fields = ("created", "mode", "is_active", "course_details", "user")
136+
fields = ("created", "mode", "is_active", "course_details", "user", "accessExpirationDate", "isAudit")
89137
lookup_field = "username"
90138

91139

@@ -106,7 +154,7 @@ class Meta(CourseEnrollmentSerializer.Meta):
106154

107155

108156
class ModeSerializer(serializers.Serializer): # pylint: disable=abstract-method
109-
"""Serializes a course's 'Mode' tuples
157+
"""Serializes a course's 'Mode' tuples.
110158
111159
Returns a serialized representation of the modes available for course enrollment. The course
112160
modes models are designed to return a tuple instead of the model object itself. This serializer

openedx/core/djangoapps/enrollments/tests/fixtures/course-enrollments-api-list-valid-data.json

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@
99
"is_active": true,
1010
"mode": "honor",
1111
"user": "student1",
12-
"created": "2018-01-01T00:00:01Z"
12+
"created": "2018-01-01T00:00:01Z",
13+
"accessExpirationDate": null,
14+
"isAudit": false
1315
},
1416
{
1517
"course_id": "course-v1:e+d+X",
1618
"is_active": true,
1719
"mode": "honor",
1820
"user": "student2",
19-
"created": "2018-01-01T00:00:01Z"
21+
"created": "2018-01-01T00:00:01Z",
22+
"accessExpirationDate": null,
23+
"isAudit": false
2024
}
2125
]
2226
],
@@ -30,21 +34,27 @@
3034
"is_active": true,
3135
"mode": "verified",
3236
"user": "staff",
33-
"created": "2018-01-01T00:00:01Z"
37+
"created": "2018-01-01T00:00:01Z",
38+
"accessExpirationDate": null,
39+
"isAudit": false
3440
},
3541
{
3642
"course_id": "course-v1:x+y+Z",
3743
"is_active": true,
3844
"mode": "honor",
3945
"user": "student2",
40-
"created": "2018-01-01T00:00:01Z"
46+
"created": "2018-01-01T00:00:01Z",
47+
"accessExpirationDate": null,
48+
"isAudit": false
4149
},
4250
{
4351
"course_id": "course-v1:x+y+Z",
4452
"is_active": true,
4553
"mode": "verified",
4654
"user": "student3",
47-
"created": "2018-01-01T00:00:01Z"
55+
"created": "2018-01-01T00:00:01Z",
56+
"accessExpirationDate": null,
57+
"isAudit": false
4858
}
4959
]
5060
],
@@ -59,14 +69,18 @@
5969
"is_active": true,
6070
"mode": "honor",
6171
"user": "student2",
62-
"created": "2018-01-01T00:00:01Z"
72+
"created": "2018-01-01T00:00:01Z",
73+
"accessExpirationDate": null,
74+
"isAudit": false
6375
},
6476
{
6577
"course_id": "course-v1:x+y+Z",
6678
"is_active": true,
6779
"mode": "verified",
6880
"user": "student3",
69-
"created": "2018-01-01T00:00:01Z"
81+
"created": "2018-01-01T00:00:01Z",
82+
"accessExpirationDate": null,
83+
"isAudit": false
7084
}
7185
]
7286
],
@@ -81,7 +95,9 @@
8195
"is_active": true,
8296
"mode": "honor",
8397
"user": "student2",
84-
"created": "2018-01-01T00:00:01Z"
98+
"created": "2018-01-01T00:00:01Z",
99+
"accessExpirationDate": null,
100+
"isAudit": false
85101
}
86102
]
87103
],
@@ -95,24 +111,29 @@
95111
"is_active": true,
96112
"mode": "verified",
97113
"user": "staff",
98-
"created": "2018-01-01T00:00:01Z"
114+
"created": "2018-01-01T00:00:01Z",
115+
"accessExpirationDate": null,
116+
"isAudit": false
99117
},
100118
{
101119
"course_id": "course-v1:e+d+X",
102120
"is_active": true,
103121
"mode": "honor",
104122
"user": "student2",
105-
"created": "2018-01-01T00:00:01Z"
123+
"created": "2018-01-01T00:00:01Z",
124+
"accessExpirationDate": null,
125+
"isAudit": false
106126
},
107127
{
108128
"course_id": "course-v1:x+y+Z",
109129
"is_active": true,
110130
"mode": "honor",
111131
"user": "student2",
112-
"created": "2018-01-01T00:00:01Z"
132+
"created": "2018-01-01T00:00:01Z",
133+
"accessExpirationDate": null,
134+
"isAudit": false
113135
}
114136
]
115-
116137
],
117138
[
118139
{
@@ -124,7 +145,9 @@
124145
"is_active": true,
125146
"mode": "honor",
126147
"user": "student1",
127-
"created": "2018-01-01T00:00:01Z"
148+
"created": "2018-01-01T00:00:01Z",
149+
"accessExpirationDate": null,
150+
"isAudit": false
128151
}
129152
]
130153
],
@@ -138,21 +161,27 @@
138161
"is_active": true,
139162
"mode": "honor",
140163
"user": "student1",
141-
"created": "2018-01-01T00:00:01Z"
164+
"created": "2018-01-01T00:00:01Z",
165+
"accessExpirationDate": null,
166+
"isAudit": false
142167
},
143168
{
144169
"course_id": "course-v1:e+d+X",
145170
"is_active": true,
146171
"mode": "honor",
147172
"user": "student2",
148-
"created": "2018-01-01T00:00:01Z"
173+
"created": "2018-01-01T00:00:01Z",
174+
"accessExpirationDate": null,
175+
"isAudit": false
149176
},
150177
{
151178
"course_id": "course-v1:x+y+Z",
152179
"is_active": true,
153180
"mode": "honor",
154181
"user": "student2",
155-
"created": "2018-01-01T00:00:01Z"
182+
"created": "2018-01-01T00:00:01Z",
183+
"accessExpirationDate": null,
184+
"isAudit": false
156185
}
157186
]
158187
],
@@ -167,7 +196,9 @@
167196
"is_active": true,
168197
"mode": "honor",
169198
"user": "student2",
170-
"created": "2018-01-01T00:00:01Z"
199+
"created": "2018-01-01T00:00:01Z",
200+
"accessExpirationDate": null,
201+
"isAudit": false
171202
}
172203
]
173204
],
@@ -179,35 +210,45 @@
179210
"is_active": true,
180211
"mode": "honor",
181212
"user": "student1",
182-
"created": "2018-01-01T00:00:01Z"
213+
"created": "2018-01-01T00:00:01Z",
214+
"accessExpirationDate": null,
215+
"isAudit": false
183216
},
184217
{
185218
"course_id": "course-v1:e+d+X",
186219
"is_active": true,
187220
"mode": "honor",
188221
"user": "student2",
189-
"created": "2018-01-01T00:00:01Z"
222+
"created": "2018-01-01T00:00:01Z",
223+
"accessExpirationDate": null,
224+
"isAudit": false
190225
},
191226
{
192227
"course_id": "course-v1:x+y+Z",
193228
"is_active": true,
194229
"mode": "verified",
195230
"user": "student3",
196-
"created": "2018-01-01T00:00:01Z"
231+
"created": "2018-01-01T00:00:01Z",
232+
"accessExpirationDate": null,
233+
"isAudit": false
197234
},
198235
{
199236
"course_id": "course-v1:x+y+Z",
200237
"is_active": true,
201238
"mode": "honor",
202239
"user": "student2",
203-
"created": "2018-01-01T00:00:01Z"
240+
"created": "2018-01-01T00:00:01Z",
241+
"accessExpirationDate": null,
242+
"isAudit": false
204243
},
205244
{
206245
"course_id": "course-v1:x+y+Z",
207246
"is_active": true,
208247
"mode": "verified",
209248
"user": "staff",
210-
"created": "2018-01-01T00:00:01Z"
249+
"created": "2018-01-01T00:00:01Z",
250+
"accessExpirationDate": null,
251+
"isAudit": false
211252
}
212253
]
213254
],
@@ -221,35 +262,45 @@
221262
"is_active": true,
222263
"mode": "honor",
223264
"user": "student1",
224-
"created": "2018-01-01T00:00:01Z"
265+
"created": "2018-01-01T00:00:01Z",
266+
"accessExpirationDate": null,
267+
"isAudit": false
225268
},
226269
{
227270
"course_id": "course-v1:e+d+X",
228271
"is_active": true,
229272
"mode": "honor",
230273
"user": "student2",
231-
"created": "2018-01-01T00:00:01Z"
274+
"created": "2018-01-01T00:00:01Z",
275+
"accessExpirationDate": null,
276+
"isAudit": false
232277
},
233278
{
234279
"course_id": "course-v1:x+y+Z",
235280
"is_active": true,
236281
"mode": "verified",
237282
"user": "staff",
238-
"created": "2018-01-01T00:00:01Z"
283+
"created": "2018-01-01T00:00:01Z",
284+
"accessExpirationDate": null,
285+
"isAudit": false
239286
},
240287
{
241288
"course_id": "course-v1:x+y+Z",
242289
"is_active": true,
243290
"mode": "honor",
244291
"user": "student2",
245-
"created": "2018-01-01T00:00:01Z"
292+
"created": "2018-01-01T00:00:01Z",
293+
"accessExpirationDate": null,
294+
"isAudit": false
246295
},
247296
{
248297
"course_id": "course-v1:x+y+Z",
249298
"is_active": true,
250299
"mode": "verified",
251300
"user": "student3",
252-
"created": "2018-01-01T00:00:01Z"
301+
"created": "2018-01-01T00:00:01Z",
302+
"accessExpirationDate": null,
303+
"isAudit": false
253304
}
254305
]
255306
],
@@ -264,14 +315,18 @@
264315
"is_active": true,
265316
"mode": "honor",
266317
"user": "student2",
267-
"created": "2018-01-01T00:00:01Z"
318+
"created": "2018-01-01T00:00:01Z",
319+
"accessExpirationDate": null,
320+
"isAudit": false
268321
},
269322
{
270323
"course_id": "course-v1:x+y+Z",
271324
"is_active": true,
272325
"mode": "honor",
273326
"user": "student2",
274-
"created": "2018-01-01T00:00:01Z"
327+
"created": "2018-01-01T00:00:01Z",
328+
"accessExpirationDate": null,
329+
"isAudit": false
275330
}
276331
]
277332
]

0 commit comments

Comments
 (0)