Skip to content

Commit f8a2333

Browse files
committed
feat: allow cancel/remind-all assignments views to be filterable.
ENT-8156 | The remind-all and cancel-all views for content assignments are now filterable by anything the list view can be filtered by, included the `learner_state` attribute.
1 parent 320bfe8 commit f8a2333

File tree

3 files changed

+173
-9
lines changed

3 files changed

+173
-9
lines changed

enterprise_access/apps/api/filters/content_assignments.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
"""
44
from django_filters import CharFilter
55

6+
from ...content_assignments.constants import AssignmentLearnerStates
67
from ...content_assignments.models import AssignmentConfiguration, LearnerContentAssignment
78
from .base import CharInFilter, HelpfulFilterSet
89

10+
LEARNER_STATE_HELP_TEXT = (
11+
'Choose from the following valid learner states: ' +
12+
', '.join([choice for choice, _ in AssignmentLearnerStates.CHOICES])
13+
)
14+
915

1016
class AssignmentConfigurationFilter(HelpfulFilterSet):
1117
"""
@@ -20,8 +26,16 @@ class LearnerContentAssignmentAdminFilter(HelpfulFilterSet):
2026
"""
2127
Base filter for LearnerContentAssignment views.
2228
"""
23-
learner_state = CharFilter(field_name='learner_state', lookup_expr='exact')
24-
learner_state__in = CharInFilter(field_name='learner_state', lookup_expr='in')
29+
learner_state = CharFilter(
30+
field_name='learner_state',
31+
lookup_expr='exact',
32+
help_text=LEARNER_STATE_HELP_TEXT,
33+
)
34+
learner_state__in = CharInFilter(
35+
field_name='learner_state',
36+
lookup_expr='in',
37+
help_text=LEARNER_STATE_HELP_TEXT,
38+
)
2539

2640
class Meta:
2741
model = LearnerContentAssignment

enterprise_access/apps/api/v1/tests/test_assignment_views.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,3 +1100,143 @@ def test_cancel_all_unlikely_thing_to_happen(self):
11001100
response = self.client.post(cancel_url)
11011101

11021102
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
1103+
1104+
1105+
class TestFilteredRemindAllCancelAll(CRUDViewTestMixin, APITest):
1106+
"""
1107+
Tests for the remind-all and cancel-all actions when filters are provided in the request query.
1108+
"""
1109+
def test_cancel_all_filter_multiple_learner_states(self):
1110+
"""
1111+
Tests the cancel-all view with a provided filter on multiple learner_states.
1112+
"""
1113+
self.set_jwt_cookie([
1114+
{'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE, 'context': str(TEST_ENTERPRISE_UUID)}
1115+
])
1116+
learner_states_to_query = [
1117+
AssignmentLearnerStates.WAITING,
1118+
AssignmentLearnerStates.FAILED,
1119+
AssignmentLearnerStates.NOTIFYING,
1120+
]
1121+
cancel_kwargs = {
1122+
'assignment_configuration_uuid': str(self.assignment_configuration.uuid),
1123+
}
1124+
cancel_url = reverse('api:v1:admin-assignments-cancel-all', kwargs=cancel_kwargs)
1125+
learner_state_query_param_value = ",".join(learner_states_to_query)
1126+
cancel_url += f'?learner_state__in={learner_state_query_param_value}'
1127+
1128+
expected_cancelled_assignments = [
1129+
self.assignment_allocated_pre_link,
1130+
self.assignment_allocated_post_link,
1131+
self.requester_assignment_errored,
1132+
]
1133+
with mock.patch(
1134+
'enterprise_access.apps.content_assignments.tasks.send_cancel_email_for_pending_assignment'
1135+
) as mock_cancel_task:
1136+
response = self.client.post(cancel_url)
1137+
1138+
assert response.status_code == status.HTTP_202_ACCEPTED
1139+
mock_cancel_task.delay.assert_has_calls(
1140+
[mock.call(assignment.uuid) for assignment in expected_cancelled_assignments],
1141+
any_order=True,
1142+
)
1143+
for assignment in expected_cancelled_assignments:
1144+
assignment.refresh_from_db()
1145+
self.assertEqual(assignment.state, LearnerContentAssignmentStateChoices.CANCELLED)
1146+
1147+
def test_cancel_all_filter_single_learner_state(self):
1148+
"""
1149+
Tests the cancel-all view with a provided filter on a single learner_state.
1150+
"""
1151+
self.set_jwt_cookie([
1152+
{'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE, 'context': str(TEST_ENTERPRISE_UUID)}
1153+
])
1154+
cancel_kwargs = {
1155+
'assignment_configuration_uuid': str(self.assignment_configuration.uuid),
1156+
}
1157+
cancel_url = reverse('api:v1:admin-assignments-cancel-all', kwargs=cancel_kwargs)
1158+
cancel_url += f'?learner_state={AssignmentLearnerStates.WAITING}'
1159+
1160+
expected_cancelled_assignments = [
1161+
self.assignment_allocated_post_link,
1162+
]
1163+
with mock.patch(
1164+
'enterprise_access.apps.content_assignments.tasks.send_cancel_email_for_pending_assignment'
1165+
) as mock_cancel_task:
1166+
response = self.client.post(cancel_url)
1167+
1168+
assert response.status_code == status.HTTP_202_ACCEPTED
1169+
mock_cancel_task.delay.assert_has_calls(
1170+
[mock.call(assignment.uuid) for assignment in expected_cancelled_assignments],
1171+
any_order=True,
1172+
)
1173+
for assignment in expected_cancelled_assignments:
1174+
assignment.refresh_from_db()
1175+
self.assertEqual(assignment.state, LearnerContentAssignmentStateChoices.CANCELLED)
1176+
1177+
def test_remind_all_filter_multiple_learner_states(self):
1178+
"""
1179+
Tests the remind-all view with a provided filter on multiple learner_states.
1180+
"""
1181+
self.set_jwt_cookie([
1182+
{'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE, 'context': str(TEST_ENTERPRISE_UUID)}
1183+
])
1184+
learner_states_to_query = [
1185+
AssignmentLearnerStates.WAITING,
1186+
AssignmentLearnerStates.FAILED,
1187+
AssignmentLearnerStates.NOTIFYING,
1188+
]
1189+
remind_kwargs = {
1190+
'assignment_configuration_uuid': str(self.assignment_configuration.uuid),
1191+
}
1192+
remind_url = reverse('api:v1:admin-assignments-remind-all', kwargs=remind_kwargs)
1193+
learner_state_query_param_value = ",".join(learner_states_to_query)
1194+
remind_url += f'?learner_state__in={learner_state_query_param_value}'
1195+
1196+
expected_reminded_assignments = [
1197+
self.assignment_allocated_pre_link,
1198+
self.assignment_allocated_post_link,
1199+
]
1200+
with mock.patch(
1201+
'enterprise_access.apps.content_assignments.api.send_reminder_email_for_pending_assignment'
1202+
) as mock_remind_task:
1203+
response = self.client.post(remind_url)
1204+
1205+
assert response.status_code == status.HTTP_202_ACCEPTED
1206+
mock_remind_task.delay.assert_has_calls(
1207+
[mock.call(assignment.uuid) for assignment in expected_reminded_assignments],
1208+
any_order=True,
1209+
)
1210+
for assignment in expected_reminded_assignments:
1211+
assignment.refresh_from_db()
1212+
self.assertEqual(assignment.state, LearnerContentAssignmentStateChoices.ALLOCATED)
1213+
1214+
def test_remind_all_filter_single_learner_state(self):
1215+
"""
1216+
Tests the remind-all view with a provided filter on a single learner_state.
1217+
"""
1218+
self.set_jwt_cookie([
1219+
{'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE, 'context': str(TEST_ENTERPRISE_UUID)}
1220+
])
1221+
remind_kwargs = {
1222+
'assignment_configuration_uuid': str(self.assignment_configuration.uuid),
1223+
}
1224+
remind_url = reverse('api:v1:admin-assignments-remind-all', kwargs=remind_kwargs)
1225+
remind_url += f'?learner_state={AssignmentLearnerStates.WAITING}'
1226+
1227+
expected_reminded_assignments = [
1228+
self.assignment_allocated_post_link,
1229+
]
1230+
with mock.patch(
1231+
'enterprise_access.apps.content_assignments.api.send_reminder_email_for_pending_assignment'
1232+
) as mock_remind_task:
1233+
response = self.client.post(remind_url)
1234+
1235+
assert response.status_code == status.HTTP_202_ACCEPTED
1236+
mock_remind_task.delay.assert_has_calls(
1237+
[mock.call(assignment.uuid) for assignment in expected_reminded_assignments],
1238+
any_order=True,
1239+
)
1240+
for assignment in expected_reminded_assignments:
1241+
assignment.refresh_from_db()
1242+
self.assertEqual(assignment.state, LearnerContentAssignmentStateChoices.ALLOCATED)

enterprise_access/apps/api/v1/views/content_assignments/assignments_admin.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def get_queryset(self):
113113
A base queryset to list or retrieve ``LearnerContentAssignment`` records.
114114
"""
115115
queryset = LearnerContentAssignment.objects.all()
116-
if self.action == 'list':
116+
if self.action in ('list', 'remind_all', 'cancel_all'):
117117
# Limit results based on the requested assignment configuration.
118118
queryset = queryset.filter(
119119
assignment_configuration__uuid=self.requested_assignment_configuration_uuid
@@ -124,12 +124,12 @@ def get_queryset(self):
124124
pass
125125

126126
# Annotate extra dynamic fields used by this viewset for DRF-supported ordering and filtering,
127-
# but only for the list and retrieve actions:
127+
# but only for the list, retrieve, and cancel/remind-all actions:
128128
# * learner_state
129129
# * learner_state_sort_order
130130
# * recent_action
131131
# * recent_action_time
132-
if self.action in ('list', 'retrieve'):
132+
if self.action in ('list', 'retrieve', 'remind_all', 'cancel_all'):
133133
queryset = LearnerContentAssignment.annotate_dynamic_fields_onto_queryset(
134134
queryset,
135135
).prefetch_related(
@@ -198,9 +198,11 @@ def cancel(self, request, *args, **kwargs):
198198
"""
199199
Cancel a list of ``LearnerContentAssignment`` records by uuid.
200200
201+
```
201202
Raises:
202203
404 if any of the assignments were not found
203204
422 if any of the assignments threw an error (not found or not cancelable)
205+
```
204206
"""
205207
serializer = LearnerContentAssignmentActionRequestSerializer(data=request.data)
206208
serializer.is_valid(raise_exception=True)
@@ -219,24 +221,27 @@ def cancel(self, request, *args, **kwargs):
219221
tags=[CONTENT_ASSIGNMENT_ADMIN_CRUD_API_TAG],
220222
summary='Cancel all assignments for the requested assignment configuration.',
221223
request=None,
224+
filters=filters.LearnerContentAssignmentAdminFilter,
222225
responses={
223226
status.HTTP_202_ACCEPTED: None,
224227
status.HTTP_404_NOT_FOUND: None,
225228
status.HTTP_422_UNPROCESSABLE_ENTITY: None,
226229
},
227230
)
228231
@permission_required(CONTENT_ASSIGNMENT_ADMIN_WRITE_PERMISSION, fn=assignment_admin_permission_fn)
229-
@action(detail=False, methods=['post'], url_path='cancel-all')
232+
@action(detail=False, methods=['post'], url_path='cancel-all', pagination_class=None)
230233
def cancel_all(self, request, *args, **kwargs):
231234
"""
232235
Cancel all ``LearnerContentAssignment`` associated with the given assignment configuration.
236+
Optionally, cancel only assignments matching the criteria of the provided query param filters.
233237
238+
```
234239
Raises:
235240
404 if any of the assignments were not found
236241
422 if any of the assignments threw an error (not found or not cancelable)
242+
```
237243
"""
238244
assignments = self.get_queryset().filter(
239-
assignment_configuration__uuid=self.requested_assignment_configuration_uuid,
240245
state__in=LearnerContentAssignmentStateChoices.CANCELABLE_STATES,
241246
)
242247
if not assignments:
@@ -275,9 +280,11 @@ def remind(self, request, *args, **kwargs):
275280
Send reminders to a list of learners with associated ``LearnerContentAssignment``
276281
record by list of uuids.
277282
283+
```
278284
Raises:
279285
404 if any of the assignments were not found
280286
422 if any of the assignments threw an error (not found or not remindable)
287+
```
281288
"""
282289
serializer = LearnerContentAssignmentActionRequestSerializer(data=request.data)
283290
serializer.is_valid(raise_exception=True)
@@ -297,24 +304,27 @@ def remind(self, request, *args, **kwargs):
297304
tags=[CONTENT_ASSIGNMENT_ADMIN_CRUD_API_TAG],
298305
summary='Remind all assignments for the given assignment configuration.',
299306
request=None,
307+
filters=filters.LearnerContentAssignmentAdminFilter,
300308
responses={
301309
status.HTTP_202_ACCEPTED: None,
302310
status.HTTP_404_NOT_FOUND: None,
303311
status.HTTP_422_UNPROCESSABLE_ENTITY: None,
304312
},
305313
)
306314
@permission_required(CONTENT_ASSIGNMENT_ADMIN_WRITE_PERMISSION, fn=assignment_admin_permission_fn)
307-
@action(detail=False, methods=['post'], url_path='remind-all')
315+
@action(detail=False, methods=['post'], url_path='remind-all', pagination_class=None)
308316
def remind_all(self, request, *args, **kwargs):
309317
"""
310318
Send reminders for all assignments related to the given assignment configuration.
319+
Optionally, remind only assignments matching the criteria of the provided query param filters.
311320
321+
```
312322
Raises:
313323
404 if any of the assignments were not found
314324
422 if any of the assignments threw an error (not found or not remindable)
325+
```
315326
"""
316327
assignments = self.get_queryset().filter(
317-
assignment_configuration__uuid=self.requested_assignment_configuration_uuid,
318328
state__in=LearnerContentAssignmentStateChoices.REMINDABLE_STATES,
319329
)
320330
if not assignments:

0 commit comments

Comments
 (0)