Skip to content

Commit

Permalink
Merge pull request #892 from edx/mohtamba/get_bulk_allowances_endpoint
Browse files Browse the repository at this point in the history
Grouped allowance endpoint
  • Loading branch information
mohtamba committed Jun 22, 2021
2 parents 2d4a91d + 0826f72 commit f6426e9
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Change Log
Unreleased
~~~~~~~~~~

[3.16.0] - 2021-06-22
~~~~~~~~~~~~~~~~~~~~~
* Created a GET api endpoint which groups course allowances by users.

[3.15.1] - 2021-06-16
~~~~~~~~~~~~~~~~~~~~~
* Fix a bug in exam attempt API where total time allowed for the exam would not include allowance time.
Expand Down
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"""

# Be sure to update the version number in edx_proctoring/package.json
__version__ = '3.15.1'
__version__ = '3.16.0'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
180 changes: 179 additions & 1 deletion edx_proctoring/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
ProctoredExamStudentAttemptHistory
)
from edx_proctoring.runtime import get_runtime_service, set_runtime_service
from edx_proctoring.serializers import ProctoredExamSerializer
from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAllowanceSerializer
from edx_proctoring.statuses import (
InstructorDashboardOnboardingAttemptStatus,
ProctoredExamStudentAttemptStatus,
Expand Down Expand Up @@ -4900,6 +4900,184 @@ def test_add_bulk_allowance_no_exams(self): # pylint: disable=invalid-name
self.assertEqual(response.status_code, 400)


class GroupedExamAllowancesByStudent(LoggedInTestCase):
"""
Tests for the GroupedExamAllowancesByStudent
"""
def setUp(self):
super().setUp()
self.user.is_staff = True
self.user.save()
self.client.login_user(self.user)
self.student_taking_exam = User()
self.student_taking_exam.save()

set_runtime_service('instructor', MockInstructorService(is_user_course_staff=True))

def test_get_grouped_allowances(self):
"""
Test to get the exam allowances of a course
"""
# Create an exam.
user_list = self.create_batch_users(3)
user_id_list = [user.email for user in user_list]
exam1 = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
time_limit_mins=90,
is_active=True
)
exam_list = [exam1.id]

allowance_data = {
'exam_ids': exam_list,
'user_ids': user_id_list,
'allowance_type': ADDITIONAL_TIME,
'value': '30'
}
self.client.put(
reverse('edx_proctoring:proctored_exam.bulk_allowance'),
json.dumps(allowance_data),
content_type='application/json'
)
url = reverse(
'edx_proctoring:proctored_exam.allowance.grouped.course',
kwargs={'course_id': exam1.course_id}
)

# Create expected dictionary by getting each users allowance seperately
first_user = user_list[0].id
first_user_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(exam1.id, first_user,
'additional_time_granted')
first_serialized_allowance = ProctoredExamStudentAllowanceSerializer(first_user_allowance).data
second_user = user_list[1].id
second_user_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(exam1.id, second_user,
'additional_time_granted')
second_serialized_allowance = ProctoredExamStudentAllowanceSerializer(second_user_allowance).data
third_user = user_list[2].id
third_user_allowance = ProctoredExamStudentAllowance.get_allowance_for_user(exam1.id, third_user,
'additional_time_granted')
third_serialized_allowance = ProctoredExamStudentAllowanceSerializer(third_user_allowance).data
expected_response = {
'grouped_allowances': {str(first_user): [first_serialized_allowance],
str(second_user): [second_serialized_allowance],
str(third_user): [third_serialized_allowance]}
}
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertDictEqual(expected_response, response_data)

def test_get_grouped_allowances_non_staff(self):
"""
Test to get the exam allowances of a course when not a staff member
"""
# Create an exam.
user_list = self.create_batch_users(3)
user_id_list = [user.email for user in user_list]
exam1 = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
time_limit_mins=90,
is_active=True
)
exam2 = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content2',
exam_name='Test Exam2',
time_limit_mins=90,
is_active=True
)
exam_list = [exam1.id, exam2.id]

allowance_data = {
'exam_ids': exam_list,
'user_ids': user_id_list,
'allowance_type': ADDITIONAL_TIME,
'value': '30'
}
self.client.put(
reverse('edx_proctoring:proctored_exam.bulk_allowance'),
json.dumps(allowance_data),
content_type='application/json'
)
self.user.is_staff = False
self.user.save()
set_runtime_service('instructor', MockInstructorService(is_user_course_staff=False))
url = reverse(
'edx_proctoring:proctored_exam.allowance.grouped.course',
kwargs={'course_id': exam1.course_id}
)
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(response_data['detail'], 'Must be a Staff User to Perform this request.')

def test_get_grouped_allowances_course_no_allowances(self):
"""
Test to get the exam allowances of a course with no allowances
"""
url = reverse(
'edx_proctoring:proctored_exam.allowance.grouped.course',
kwargs={'course_id': 'a/c/d'}
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(len(response_data), 1)
grouped_allowances = response_data['grouped_allowances']
self.assertEqual(len(grouped_allowances), 0)

def test_get_grouped_allowances_non_global_staff(self):
"""
Test to get the exam allowances of a course when a member is course staff,
but not global
"""
# Create an exam.
user_list = self.create_batch_users(3)
user_id_list = [user.email for user in user_list]
exam1 = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content',
exam_name='Test Exam',
time_limit_mins=90,
is_active=True
)
exam2 = ProctoredExam.objects.create(
course_id='a/b/c',
content_id='test_content2',
exam_name='Test Exam2',
time_limit_mins=90,
is_active=True
)
exam_list = [exam1.id, exam2.id]

allowance_data = {
'exam_ids': exam_list,
'user_ids': user_id_list,
'allowance_type': ADDITIONAL_TIME,
'value': '30'
}
self.client.put(
reverse('edx_proctoring:proctored_exam.bulk_allowance'),
json.dumps(allowance_data),
content_type='application/json'
)
set_runtime_service('instructor', MockInstructorService(is_user_course_staff=False))
url = reverse(
'edx_proctoring:proctored_exam.allowance.grouped.course',
kwargs={'course_id': exam1.course_id}
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
self.assertEqual(len(response_data), 1)
grouped_allowances = response_data['grouped_allowances']
self.assertEqual(len(grouped_allowances), 3)


class TestActiveExamsForUserView(LoggedInTestCase):
"""
Tests for the ActiveExamsForUserView
Expand Down
5 changes: 5 additions & 0 deletions edx_proctoring/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
views.ExamBulkAllowanceView.as_view(),
name='proctored_exam.bulk_allowance'
),
url(
r'edx_proctoring/v1/proctored_exam/allowance/grouped/course_id/{}$'.format(settings.COURSE_ID_PATTERN),
views.GroupedExamAllowancesByStudent.as_view(),
name='proctored_exam.allowance.grouped.course'
),
url(
r'edx_proctoring/v1/proctored_exam/active_exams_for_user$',
views.ActiveExamsForUserView.as_view(),
Expand Down
56 changes: 55 additions & 1 deletion edx_proctoring/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@
ProctoredExamStudentAttemptHistory
)
from edx_proctoring.runtime import get_runtime_service
from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAttemptSerializer
from edx_proctoring.serializers import (
ProctoredExamSerializer,
ProctoredExamStudentAllowanceSerializer,
ProctoredExamStudentAttemptSerializer
)
from edx_proctoring.statuses import (
InstructorDashboardOnboardingAttemptStatus,
ProctoredExamStudentAttemptStatus,
Expand Down Expand Up @@ -1456,6 +1460,56 @@ def put(self, request):
)


class GroupedExamAllowancesByStudent(ProctoredAPIView):
"""
Endpoint for the StudentProctoredGroupedExamAttemptsByCourse
Supports:
HTTP GET: return information about learners' allowances
**Expected Response**
HTTP GET:
The response will contain a dictionary with the allowances of a course grouped by student.
For example:
{'grouped_allowances':{'4':
[{'id': 4, 'created': '2021-06-21T14:47:17.847221Z',
'modified': '2021-06-21T14:47:17.847221Z',
'user': {'id': 4, 'username': 'student1',
'email': '[email protected]'},
'key': 'additional_time_granted', 'value': '30',
'proctored_exam': {'id': 2, 'course_id': 'a/b/c', 'content_id': 'test_content2',
'external_id': None, 'exam_name': 'Test Exam2', 'time_limit_mins': 90,
'is_proctored': False, 'is_practice_exam': False,
'is_active': True, 'due_date': None,
'hide_after_due': False, 'backend': None}}}
**Exceptions**
HTTP GET:
* 403 if the requesting user is not staff or course staff for the course associated with
the supplied course ID
"""
@method_decorator(require_course_or_global_staff)
def get(self, request, course_id):
"""
HTTP GET Handler.
"""

# Get all allowances from the course
all_allowances = ProctoredExamStudentAllowance.get_allowances_for_course(course_id)

grouped_allowances = {}

# Process allowances so they are grouped by user id
for allowance in all_allowances:
serialied_allowance = ProctoredExamStudentAllowanceSerializer(allowance).data
user_id = serialied_allowance['user']['id']
grouped_allowances.setdefault(user_id, []).append(serialied_allowance)

response_data = {'grouped_allowances': grouped_allowances}

return Response(response_data)


class ActiveExamsForUserView(ProctoredAPIView):
"""
Endpoint for the Active Exams for a user.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@edx/edx-proctoring",
"//": "Be sure to update the version number in edx_proctoring/__init__.py",
"//": "Note that the version format is slightly different than that of the Python version when using prereleases.",
"version": "3.15.1",
"version": "3.16.0",
"main": "edx_proctoring/static/index.js",
"scripts": {
"test": "gulp test"
Expand Down

0 comments on commit f6426e9

Please sign in to comment.