From 6c8c32e8802ec930917c3ad31034d91d9c8f14e8 Mon Sep 17 00:00:00 2001 From: Alie Langston Date: Mon, 22 Jul 2024 09:12:11 -0400 Subject: [PATCH] feat: add view for allowance creation --- .gitignore | 2 + edx_exams/apps/api/v1/tests/test_views.py | 50 +++++++++++++++++++++-- edx_exams/apps/api/v1/views.py | 45 +++++++++++++++++++- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 24aefe00..11cfa2a8 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,5 @@ docs/_build/ # Temp reports file reports/ + +requirements/private.txt diff --git a/edx_exams/apps/api/v1/tests/test_views.py b/edx_exams/apps/api/v1/tests/test_views.py index da1be480..d3662b53 100644 --- a/edx_exams/apps/api/v1/tests/test_views.py +++ b/edx_exams/apps/api/v1/tests/test_views.py @@ -17,7 +17,14 @@ from edx_exams.apps.api.test_utils import ExamsAPITestCase from edx_exams.apps.core.exam_types import get_exam_type from edx_exams.apps.core.exceptions import ExamAttemptOnPastDueExam, ExamIllegalStatusTransition -from edx_exams.apps.core.models import CourseExamConfiguration, CourseStaffRole, Exam, ExamAttempt, ProctoringProvider +from edx_exams.apps.core.models import ( + CourseExamConfiguration, + CourseStaffRole, + Exam, + ExamAttempt, + ProctoringProvider, + StudentAllowance +) from edx_exams.apps.core.statuses import ExamAttemptStatus from edx_exams.apps.core.test_utils.factories import ( AssessmentControlResultFactory, @@ -1768,18 +1775,21 @@ def setUp(self): course_id=self.course_id, ) - def request_api(self, method, user, course_id): + def request_api(self, method, user, course_id, data=None): """ Helper function to make API request """ - assert method in ['get'] + assert method in ['get', 'post'] headers = self.build_jwt_headers(user) url = reverse( 'api:v1:course-allowances', kwargs={'course_id': course_id} ) - return getattr(self.client, method)(url, **headers) + if data: + return getattr(self.client, method)(url, json.dumps(data), **headers, content_type='application/json') + else: + return getattr(self.client, method)(url, **headers) def test_auth_required(self): """ @@ -1851,3 +1861,35 @@ def test_get_empty_response(self): response = self.request_api('get', self.user, 'course-v1:edx+no+allowances') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, []) + + def test_post_allowances(self): + """ + Test that the endpoint creates allowances for the given request data + """ + other_exam_in_course = ExamFactory.create(course_id=self.exam.course_id) + other_user = UserFactory() + StudentAllowanceFactory.create( + exam=self.exam, + user=self.user, + extra_time_mins=30, + ) + StudentAllowanceFactory.create( + exam=other_exam_in_course, + user=self.user, + extra_time_mins=30, + ) + + request_data = [ + {'exam_id': self.exam.id, 'username': self.user.username, 'extra_time_mins': 45}, + {'exam_id': other_exam_in_course.id, 'username': self.user.username, 'extra_time_mins': 45}, + {'exam_id': other_exam_in_course.id, 'username': other_user.username, 'extra_time_mins': 45}, + ] + + response = self.request_api('post', self.user, self.exam.course_id, data=request_data) + self.assertEqual(response.status_code, 200) + + course_allowances = StudentAllowance.objects.all() + self.assertEqual(len(course_allowances), 3) + + self.assertEqual(len(StudentAllowance.objects.filter(user_id=self.user.id)), 2) + self.assertEqual(len(StudentAllowance.objects.filter(extra_time_mins=45)), 3) diff --git a/edx_exams/apps/api/v1/views.py b/edx_exams/apps/api/v1/views.py index 533fbaea..337ca86c 100644 --- a/edx_exams/apps/api/v1/views.py +++ b/edx_exams/apps/api/v1/views.py @@ -46,7 +46,14 @@ update_attempt_status ) from edx_exams.apps.core.exam_types import get_exam_type -from edx_exams.apps.core.models import CourseExamConfiguration, Exam, ExamAttempt, ProctoringProvider, StudentAllowance +from edx_exams.apps.core.models import ( + CourseExamConfiguration, + Exam, + ExamAttempt, + ProctoringProvider, + StudentAllowance, + User +) from edx_exams.apps.core.statuses import ExamAttemptStatus from edx_exams.apps.router.interop import get_active_exam_attempt @@ -784,13 +791,25 @@ def get(self, request, course_id, exam_id): class AllowanceView(ExamsAPIView): """ - Endpoint for getting allowances in a course + Endpoint for the StudentAllowance /exams/course_id/{course_id}/allowances Supports: HTTP GET: Returns a list of allowances for a course. + HTTP POST: + Create one or more allowances + + Expected POST data: [{ + "username": "test_user", + "exam_id": 1234, + "extra_time_mins": 30, + }] + **POST data Parameters** + * username: Username for which to create or update an allowance. + * exam_id: ID of the exam for which to create or update an allowance + * extra_time_mins: Extra time (in minutes) that a student is allotted for an exam. """ authentication_classes = (JwtAuthentication,) @@ -802,3 +821,25 @@ def get(self, request, course_id): """ allowances = StudentAllowance.get_allowances_for_course(course_id) return Response(AllowanceSerializer(allowances, many=True).data) + + def post(self, request, course_id): # pylint: disable=unused-argument + """ + HTTP POST handler. Creates allowances based on the given list. + """ + allowances = request.data + allowance_objects = [ + StudentAllowance( + user=User.objects.get(username=allowance['username']), + exam=Exam.objects.get(id=allowance['exam_id']), + extra_time_mins=allowance['extra_time_mins'] + ) + for allowance in allowances + ] + StudentAllowance.objects.bulk_create( + allowance_objects, + update_conflicts=True, + unique_fields=['user', 'exam'], + update_fields=['extra_time_mins'] + ) + + return Response(status=status.HTTP_200_OK)