Skip to content

Commit

Permalink
feat: add view for allowance creation
Browse files Browse the repository at this point in the history
  • Loading branch information
alangsto committed Jul 22, 2024
1 parent 4cf5d52 commit 0ce433a
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,5 @@ docs/_build/

# Temp reports file
reports/

requirements/private.txt
10 changes: 5 additions & 5 deletions edx_exams/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,13 @@ class AllowanceSerializer(serializers.ModelSerializer):

# directly from the Allowance Model
id = serializers.IntegerField(required=False)
exam_id = serializers.IntegerField()
user_id = serializers.IntegerField()
extra_time_mins = serializers.IntegerField()
exam_id = serializers.IntegerField(required=True)
user_id = serializers.IntegerField(required=True)
extra_time_mins = serializers.IntegerField(required=True)

# custom fields based on related models
username = serializers.CharField(source='user.username')
exam_name = serializers.CharField(source='exam.exam_name')
username = serializers.CharField(source='user.username', required=False)
exam_name = serializers.CharField(source='exam.exam_name', required=False)

class Meta:
"""
Expand Down
60 changes: 56 additions & 4 deletions edx_exams/apps/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -1851,3 +1861,45 @@ 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, 'user_id': self.user.id, 'extra_time_mins': 45},
{'exam_id': other_exam_in_course.id, 'user_id': self.user.id, 'extra_time_mins': 45},
{'exam_id': other_exam_in_course.id, 'user_id': other_user.id, '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)

def test_invalid_post(self):
"""
Test that 400 response is returned if serializer is invalid
"""
request_data = [
{'exam_id': self.exam.id, 'user_id': self.user.id, 'extra_time_mins': 'yyyy'},
]
response = self.request_api('post', self.user, self.exam.course_id, data=request_data)
self.assertEqual(response.status_code, 400)
53 changes: 51 additions & 2 deletions edx_exams/apps/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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: [{
"user_id": 11111,
"exam_id": 1234,
"extra_time_mins": 30,
}]
**POST data Parameters**
* user_id: User ID 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,)
Expand All @@ -802,3 +821,33 @@ 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

serializer = AllowanceSerializer(data=allowances, many=True)

if serializer.is_valid():
allowance_objects = [
StudentAllowance(
user=User.objects.get(id=allowance['user_id']),
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)
else:
response_status = status.HTTP_400_BAD_REQUEST
data = {'detail': 'Invalid data', 'errors': serializer.errors}
return Response(status=response_status, data=data)

0 comments on commit 0ce433a

Please sign in to comment.