Skip to content

Commit

Permalink
fix: exam content will not be viewable if due date has passed
Browse files Browse the repository at this point in the history
Previously, if a learner had submitted their proctored exam and if the
exam due date had passed, they were able to acknowledge their status
and view the exam content. This is no longer wanted and poses an integrity
threat. Students will not be able to acknowledge their status after this change,
and exam content will only be viewable if the django setting PROCTORED_EXAM_VIEWABLE_PAST_DUE
is set to True.
  • Loading branch information
alangsto committed Jun 4, 2021
1 parent b1a6f23 commit 0e3a9ba
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 4 deletions.
7 changes: 4 additions & 3 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2281,7 +2281,8 @@ def _get_proctored_exam_context(exam, attempt, user_id, course_id, is_practice_e
'integration_specific_email': get_integration_specific_email(provider),
'exam_display_name': exam['exam_name'],
'reset_link': password_url,
'ping_interval': provider.ping_interval
'ping_interval': provider.ping_interval,
'can_view_content_past_due': getattr(settings, 'PROCTORED_EXAM_VIEWABLE_PAST_DUE', False)
}
if attempt:
context['exam_code'] = attempt['attempt_code']
Expand Down Expand Up @@ -2507,10 +2508,10 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
elif attempt_status == ProctoredExamStudentAttemptStatus.timed_out:
raise NotImplementedError('There is no defined rendering for ProctoredExamStudentAttemptStatus.timed_out!')
elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
student_view_template = None if _was_review_status_acknowledged(
student_view_template = None if (_was_review_status_acknowledged(
attempt['is_status_acknowledged'],
exam
) else 'proctored_exam/submitted.html'
) and getattr(settings, 'PROCTORED_EXAM_VIEWABLE_PAST_DUE', False)) else 'proctored_exam/submitted.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.second_review_required:
# the student should still see a 'submitted'
# rendering even if the review needs a 2nd review
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% load i18n %}
{% if has_due_date_passed %}
{% if has_due_date_passed and can_view_content_past_due %}
<hr>
<p>
{% blocktrans %}
Expand Down
52 changes: 52 additions & 0 deletions edx_proctoring/tests/test_student_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from freezegun import freeze_time
from mock import MagicMock, patch

from django.test.utils import override_settings
from django.urls import reverse

from edx_proctoring.api import (
Expand Down Expand Up @@ -81,6 +82,7 @@ def setUp(self):
self.timed_footer_msg = 'Can I request additional time to complete my exam?'
self.wait_deadline_msg = "The result will be visible after"
self.inactive_account_msg = "You have not activated your account"
self.review_exam_msg = "To view your exam questions and responses"

def _render_exam(self, content_id, context_overrides=None):
"""
Expand Down Expand Up @@ -861,6 +863,52 @@ def test_get_studentview_submitted_status(self):
rendered_response = self.render_proctored_exam()
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)

@override_settings(PROCTORED_EXAM_VIEWABLE_PAST_DUE=False)
@ddt.data(
True,
False,
)
def test_get_studentview_submitted_status_without_viewable_content(self, status_acknowledged):
"""
Test for get_student_view proctored exam which has been submitted
but exam content is not viewable if the due date has passed
"""
due_date = datetime.now(pytz.UTC) + timedelta(minutes=40)
exam_id = self._create_exam_with_due_time(
is_proctored=True, due_date=due_date
)

exam_attempt = ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=exam_id,
user=self.user,
allowed_time_limit_mins=30,
taking_as_proctored=True,
external_id='fdage332',
status=ProctoredExamStudentAttemptStatus.submitted,
)

exam_attempt.is_status_acknowledged = status_acknowledged
exam_attempt.save()

# due date is after 10 minutes
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=60)
with freeze_time(reset_time):
rendered_response = get_student_view(
user_id=self.user.id,
course_id=self.course_id,
content_id=self.content_id_for_exam_with_due_date,
context={
'is_proctored': True,
'display_name': 'Test Exam',
'default_time_limit_mins': 30,
'due_date': due_date
}
)
self.assertIsNotNone(rendered_response)
self.assertNotIn(self.review_exam_msg, rendered_response)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)

@override_settings(PROCTORED_EXAM_VIEWABLE_PAST_DUE=True)
@ddt.data(
60,
20,
Expand Down Expand Up @@ -906,6 +954,9 @@ def test_get_studentview_submitted_status_with_duedate_status_acknowledged(self,
}
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
if due_date_passed:
self.assertIn(self.review_exam_msg, rendered_response)

exam_attempt.is_status_acknowledged = True
exam_attempt.save()

Expand All @@ -925,6 +976,7 @@ def test_get_studentview_submitted_status_with_duedate_status_acknowledged(self,
else:
self.assertIsNotNone(rendered_response)

@override_settings(PROCTORED_EXAM_VIEWABLE_PAST_DUE=True)
@patch('edx_when.api.get_date_for_block')
def test_get_studentview_submitted_personalize_scheduled_duedate_status_acknowledged(self, get_date_for_block_mock):
"""
Expand Down
22 changes: 22 additions & 0 deletions edx_proctoring/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from django.contrib.auth import get_user_model
from django.test.client import Client
from django.test.utils import override_settings
from django.urls import NoReverseMatch, reverse
from django.utils import timezone

Expand Down Expand Up @@ -3541,6 +3542,25 @@ def _create_proctored_exam_attempt_with_duedate(self, due_date=datetime.now(pytz
status=ProctoredExamStudentAttemptStatus.started
)

def test_attempt_review_status_callback_non_reviewable(self):
"""
Test the ProctoredExamAttemptReviewStatus view
"""
attempt = self._create_proctored_exam_attempt_with_duedate(
due_date=datetime.now(pytz.UTC) + timedelta(minutes=40)
)

response = self.client.put(
reverse(
'edx_proctoring:proctored_exam.attempt.review_status',
args=[attempt.id]
),
{},
content_type='application/json'
)
self.assertEqual(response.status_code, 404)

@override_settings(PROCTORED_EXAM_VIEWABLE_PAST_DUE=True)
def test_attempt_review_status_callback(self):
"""
Test the ProctoredExamAttemptReviewStatus view
Expand All @@ -3559,6 +3579,7 @@ def test_attempt_review_status_callback(self):
)
self.assertEqual(response.status_code, 200)

@override_settings(PROCTORED_EXAM_VIEWABLE_PAST_DUE=True)
def test_attempt_review_status_callback_with_doesnotexit_exception(self):
"""
Test the ProctoredExamAttemptReviewStatus view with does not exit exception
Expand All @@ -3578,6 +3599,7 @@ def test_attempt_review_status_callback_with_doesnotexit_exception(self):
self.assertEqual(response.status_code, 400)
self.assertRaises(StudentExamAttemptDoesNotExistsException)

@override_settings(PROCTORED_EXAM_VIEWABLE_PAST_DUE=True)
def test_attempt_review_status_callback_with_permission_exception(self):
"""
Test the ProctoredExamAttemptReviewStatus view with permission exception
Expand Down
7 changes: 7 additions & 0 deletions edx_proctoring/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
Expand Down Expand Up @@ -1360,6 +1361,12 @@ def put(self, request, attempt_id): # pylint: disable=unused-argument
"""
Update the is_status_acknowledged flag for the specific attempt
"""
if not getattr(settings, 'PROCTORED_EXAM_VIEWABLE_PAST_DUE', False):
return Response(
status=404,
data={'detail': _('Cannot update attempt review status')}
)

attempt = get_exam_attempt_by_id(attempt_id)

# make sure the the attempt belongs to the calling user_id
Expand Down
2 changes: 2 additions & 0 deletions test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
CONTACT_EMAIL = 'info@edx.org'
TECH_SUPPORT_EMAIL = 'technical@example.com'

PROCTORED_EXAM_VIEWABLE_PAST_DUE = False

########## TEMPLATE CONFIGURATION
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
Expand Down

0 comments on commit 0e3a9ba

Please sign in to comment.