Skip to content

Commit 54363d2

Browse files
fix: Overwrite allowances in special exams instead of creating a new … (#38488)
1 parent 0df950c commit 54363d2

2 files changed

Lines changed: 85 additions & 12 deletions

File tree

lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
add_allowance_for_user,
1313
create_exam,
1414
create_exam_attempt,
15+
get_allowances_for_course,
1516
)
1617
from edx_proctoring.models import ProctoredExamStudentAttempt
1718
from rest_framework import status
@@ -428,6 +429,33 @@ def test_update_allowance(self):
428429
assert response.status_code == status.HTTP_200_OK
429430
assert response.json()['results'][0]['success'] is True
430431

432+
def test_grant_allowance_replaces_different_key(self):
433+
"""Granting an allowance with a different key replaces the existing one (one per user+exam)."""
434+
self.client.post(
435+
self._url(),
436+
data={
437+
'user_ids': [self.student.username],
438+
'allowance_type': 'additional_time_granted',
439+
'value': '30',
440+
},
441+
format='json',
442+
)
443+
response = self.client.post(
444+
self._url(),
445+
data={
446+
'user_ids': [self.student.username],
447+
'allowance_type': 'review_policy_exception',
448+
'value': 'special review',
449+
},
450+
format='json',
451+
)
452+
assert response.status_code == status.HTTP_200_OK
453+
assert response.json()['results'][0]['success'] is True
454+
allowances = get_allowances_for_course(self.course_id)
455+
user_allowances = [a for a in allowances if a['user']['username'] == self.student.username]
456+
assert len(user_allowances) == 1
457+
assert user_allowances[0]['key'] == 'review_policy_exception'
458+
431459
def test_delete_allowance(self):
432460
add_allowance_for_user(self.exam_id, self.student.username, 'additional_time_granted', '30')
433461
response = self.client.delete(
@@ -548,6 +576,26 @@ def test_bulk_create_allowances(self):
548576
assert len(data['results']) == 4
549577
assert all(r['success'] is True for r in data['results'])
550578

579+
def test_bulk_create_allowances_replaces_different_key(self):
580+
"""Bulk-creating an allowance with a different key replaces the existing one."""
581+
add_allowance_for_user(self.exam_id, self.student.username, 'additional_time_granted', '30')
582+
response = self.client.post(
583+
self._url(),
584+
data={
585+
'exam_ids': [self.exam_id],
586+
'user_ids': [self.student.username],
587+
'allowance_type': 'review_policy_exception',
588+
'value': 'special review',
589+
},
590+
format='json',
591+
)
592+
assert response.status_code == status.HTTP_200_OK
593+
assert response.json()['results'][0]['success'] is True
594+
allowances = get_allowances_for_course(self.course_id)
595+
user_allowances = [a for a in allowances if a['user']['username'] == self.student.username]
596+
assert len(user_allowances) == 1
597+
assert user_allowances[0]['key'] == 'review_policy_exception'
598+
551599
def test_bulk_create_allowances_missing_fields(self):
552600
response = self.client.post(
553601
self._url(),

lms/djangoapps/instructor/views/api_v2.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
ProctoredBaseException,
4444
ProctoredExamNotFoundException,
4545
)
46+
from edx_proctoring.models import ProctoredExamStudentAllowance
4647
from edx_rest_framework_extensions.paginators import DefaultPagination
4748
from edx_when import api as edx_when_api
4849
from opaque_keys import InvalidKeyError
@@ -4233,6 +4234,23 @@ def patch(self, request, course_id):
42334234
return Response(serializer.data, status=status.HTTP_200_OK)
42344235

42354236

4237+
def add_or_replace_allowance_for_user(exam_id, username_or_email, key, value):
4238+
"""
4239+
Add an allowance for a user on an exam, removing any existing allowance with a different key.
4240+
4241+
Enforces one allowance per user per exam regardless of allowance type. If the user already
4242+
has an allowance for this exam with a different key, it is removed before the new one is created.
4243+
"""
4244+
user_id = get_user_by_username_or_email(username_or_email).id
4245+
4246+
with transaction.atomic():
4247+
for allowance in ProctoredExamStudentAllowance.get_allowances_for_user(exam_id, user_id):
4248+
if allowance.key != key:
4249+
remove_allowance_for_user(exam_id, user_id, allowance.key)
4250+
4251+
add_allowance_for_user(exam_id, username_or_email, key, value)
4252+
4253+
42364254
class ExamAllowanceView(DeveloperErrorViewMixin, APIView):
42374255
"""
42384256
Grant, update, or remove an allowance for a student on a proctored exam.
@@ -4289,17 +4307,17 @@ def post(self, request, course_id, exam_id):
42894307

42904308
validated = serializer.validated_data
42914309
results = []
4292-
for user_info in validated['user_ids']:
4310+
for username_or_email in validated['user_ids']:
42934311
try:
4294-
add_allowance_for_user(
4312+
add_or_replace_allowance_for_user(
42954313
int(exam_id),
4296-
user_info,
4314+
username_or_email,
42974315
validated['allowance_type'],
42984316
validated['value'],
42994317
)
4300-
results.append({'identifier': user_info, 'success': True})
4301-
except ProctoredBaseException as err:
4302-
results.append({'identifier': user_info, 'success': False, 'error': str(err)})
4318+
results.append({'identifier': username_or_email, 'success': True})
4319+
except (ProctoredBaseException, User.DoesNotExist, User.MultipleObjectsReturned) as err:
4320+
results.append({'identifier': username_or_email, 'success': False, 'error': str(err)})
43034321

43044322
return Response(
43054323
{'allowance_type': validated['allowance_type'], 'results': results},
@@ -4471,17 +4489,24 @@ def post(self, request, course_id):
44714489
validated = serializer.validated_data
44724490
results = []
44734491
for exam_id in validated['exam_ids']:
4474-
for user_info in validated['user_ids']:
4492+
for username_or_email in validated['user_ids']:
44754493
try:
4476-
add_allowance_for_user(
4494+
add_or_replace_allowance_for_user(
44774495
exam_id,
4478-
user_info,
4496+
username_or_email,
44794497
validated['allowance_type'],
44804498
validated['value'],
44814499
)
4482-
results.append({'identifier': user_info, 'exam_id': exam_id, 'success': True})
4483-
except ProctoredBaseException as err:
4484-
results.append({'identifier': user_info, 'exam_id': exam_id, 'success': False, 'error': str(err)})
4500+
results.append({'identifier': username_or_email, 'exam_id': exam_id, 'success': True})
4501+
except (ProctoredBaseException, User.DoesNotExist, User.MultipleObjectsReturned) as err:
4502+
results.append(
4503+
{
4504+
'identifier': username_or_email,
4505+
'exam_id': exam_id,
4506+
'success': False,
4507+
'error': str(err)
4508+
}
4509+
)
44854510

44864511
return Response({
44874512
'allowance_type': validated['allowance_type'],

0 commit comments

Comments
 (0)