Skip to content

Commit

Permalink
Merge pull request #597 from edx/dsheraz/prod-356
Browse files Browse the repository at this point in the history
add grace period checks in edx-proctoring
  • Loading branch information
DawoudSheraz authored Sep 12, 2019
2 parents 90ce599 + d43076e commit a179233
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 64 deletions.
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
from __future__ import absolute_import

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

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
33 changes: 10 additions & 23 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
from edx_proctoring.serializers import (ProctoredExamReviewPolicySerializer, ProctoredExamSerializer,
ProctoredExamStudentAllowanceSerializer, ProctoredExamStudentAttemptSerializer)
from edx_proctoring.statuses import ProctoredExamStudentAttemptStatus
from edx_proctoring.utils import emit_event, humanized_time, obscured_user_id
from edx_when import api as when_api
from edx_proctoring.utils import (emit_event, get_exam_due_date, has_due_date_passed, humanized_time,
obscured_user_id, verify_and_add_wait_deadline)

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -513,26 +513,6 @@ def update_exam_attempt(attempt_id, **kwargs):
exam_attempt_obj.save()


def has_due_date_passed(due_datetime):
"""
return True if due date is lesser than current datetime, otherwise False
and if due_datetime is None then we don't have to consider the due date for return False
"""

if due_datetime:
return due_datetime <= datetime.now(pytz.UTC)
return False


def get_exam_due_date(exam, user=None):
"""
Return the due date for the exam.
Uses edx_when to lookup the date for the subsection.
"""
due_date = when_api.get_date_for_block(exam['course_id'], exam['content_id'], 'due', user=user)
return due_date or exam['due_date']


def is_exam_passed_due(exam, user=None):
"""
Return whether the due date has passed.
Expand Down Expand Up @@ -1623,7 +1603,9 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
# check if the exam's due_date has passed. If so, return None
# so that the user can see their exam answers in read only mode.
if not exam['hide_after_due'] and is_exam_passed_due(exam, user=user_id):
return None
has_context_updated = verify_and_add_wait_deadline(context, exam, user_id)
if not has_context_updated:
return None

student_view_template = 'timed_exam/submitted.html'

Expand Down Expand Up @@ -1963,6 +1945,11 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
attempt['is_status_acknowledged'],
exam
) else 'proctored_exam/verified.html'
has_context_updated = verify_and_add_wait_deadline(context, exam, user_id)
# The edge case where student has already acknowledged the result
# but the course team changed the grace period
if has_context_updated and not student_view_template:
student_view_template = 'proctored_exam/verified.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.rejected:
student_view_template = None if _was_review_status_acknowledged(
attempt['is_status_acknowledged'],
Expand Down
12 changes: 12 additions & 0 deletions edx_proctoring/templates/common/waiting_banner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% load i18n %}
<p>
{% blocktrans %}
The result will be visible after <strong id="wait_deadline"> Loading... </strong>
{% endblocktrans %}
</p>
<script type="text/javascript">
$(function () {
var timeZoneAwareDateString = (moment.utc('{{ wait_deadline }}').local().format('MMM DD, YYYY, LT'));
$("#wait_deadline").text(timeZoneAwareDateString);
});
</script>
26 changes: 15 additions & 11 deletions edx_proctoring/templates/proctored_exam/verified.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
{% load i18n %}
<div class="success sequence proctored-exam passed" data-exam-id="{{exam_id}}">
<h3>
{% block title %}
{% blocktrans %}
Your proctoring session was reviewed successfully. Go to your progress page to view your exam grade.
{% endblocktrans %}
{% endblock %}
</h3>
<div class="success sequence proctored-exam passed" data-exam-id="{{ exam_id }}">
<h3>
{% block title %}
{% blocktrans %}
Your proctoring session was reviewed successfully. Go to your progress page to view your exam grade.
{% endblocktrans %}
{% endblock %}
</h3>

{% block body %}
{% include 'proctored_exam/visit_exam_content.html' %}
{% endblock %}
{% block body %}
{% if wait_deadline %}
{% include 'common/waiting_banner.html' %}
{% else %}
{% include 'proctored_exam/visit_exam_content.html' %}
{% endif %}
{% endblock %}
</div>
{% include 'proctored_exam/footer.html' %}
44 changes: 26 additions & 18 deletions edx_proctoring/templates/timed_exam/submitted.html
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
{% load i18n %}
<div class="sequence proctored-exam completed" data-exam-id="{{exam_id}}">
<h3>
<div class="sequence proctored-exam completed" data-exam-id="{{ exam_id }}">
<h3>

{% if has_time_expired %}
{% blocktrans %}
The time allotted for this exam has expired. Your exam has been submitted and any work you completed will be graded.
{% endblocktrans %}
{% else %}
{% blocktrans %}
You have submitted your timed exam.
{% endblocktrans %}
{% endif %}
{% if has_time_expired %}
{% blocktrans %}
The time allotted for this exam has expired. Your exam has been submitted and any work you completed
will be graded.
{% endblocktrans %}
{% else %}
{% blocktrans %}
You have submitted your timed exam.
{% endblocktrans %}
{% endif %}

</h3>
<hr>

</h3>
<hr>
<p>
{% if will_be_revealed %}
{% blocktrans %}
After the due date has passed, you can review the exam, but you cannot change your answers.
{% endblocktrans %}
<p>
{% blocktrans %}
After the due date has passed, you can review the exam, but you cannot change your answers.
{% endblocktrans %}
</p>

{% if wait_deadline %}
{% include 'common/waiting_banner.html' %}
{% endif %}

{% endif %}
</p>
</div>

111 changes: 111 additions & 0 deletions edx_proctoring/tests/test_student_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def setUp(self):
self.wrong_browser_msg = 'The content of this exam can only be viewed'
self.footer_msg = 'About Proctored Exams'
self.timed_footer_msg = 'Can I request additional time to complete my exam?'
self.wait_deadline_msg = "The result will be visible after"

def _render_exam(self, content_id, context_overrides=None):
"""
Expand Down Expand Up @@ -589,6 +590,116 @@ def test_get_studentview_submitted_timed_exam_with_past_due_date(self, due_date,
else:
self.assertIsNone(rendered_response)

@ddt.data(
(False, 'submitted', True, 1),
(True, 'verified', False, 1),
(False, 'submitted', True, 0),
(True, 'verified', False, 0),
)
@ddt.unpack
def test_get_studentview_submitted_timed_exam_with_grace_period(self, is_proctored, status, is_timed, graceperiod):
"""
Test the student view for a submitted exam, after the
due date, when grace period is in effect.
Scenario: Given an exam with past due
When a user submission exists for that exam
Then get the user view with an active grace period
Then user will not be able to see exam content
And a banner will be visible
If the grace period is past due
For timed exam, user will not see any banner
And user will be able to see exam contents
And For proctored exam, view exam button will be visible
"""
due_date = datetime.now(pytz.UTC) - timedelta(days=1)
context = {
'is_proctored': is_proctored,
'display_name': self.exam_name,
'default_time_limit_mins': 10,
'due_date': due_date,
'grace_period': timedelta(days=2)
}
exam_id = self._create_exam_with_due_time(
is_proctored=is_proctored, due_date=due_date
)
self._create_exam_attempt(exam_id, status=status)
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=context
)
self.assertIn(self.wait_deadline_msg, rendered_response)

# This pop is required as the student view updates the
# context dict that was passed in the arguments
context.pop('wait_deadline')

context['grace_period'] = timedelta(days=graceperiod)
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=context
)
if is_timed:
self.assertIsNone(rendered_response)
else:
self.assertNotIn(self.wait_deadline_msg, rendered_response)

def test_get_studentview_acknowledged_proctored_exam_with_grace_period(self):
"""
Verify the student view for an acknowledge proctored exam with an active
grace period.
Given a proctored exam with a past due date and an inactive grace period
And a verified user submission exists for that exam
When user navigates to the exam
Then the wait deadline part is not shown
If the attempt is acknowledged to view the exam result
Then visiting the page again will not show any banner
When an active grace period is applied
Then navigating to the exam will not exam content
And the wait deadline will be shown
"""
due_date = datetime.now(pytz.UTC) - timedelta(days=1)
context = {
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 10,
'due_date': due_date,
'grace_period': timedelta(days=0)
}
exam_id = self._create_exam_with_due_time(
is_proctored=True, due_date=due_date
)
attempt = self._create_exam_attempt(exam_id, status='verified')
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=context
)
self.assertNotIn(self.wait_deadline_msg, rendered_response)
attempt.is_status_acknowledged = True
attempt.save()
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=context
)
self.assertIsNone(rendered_response)
context['grace_period'] = timedelta(days=2)
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=context
)
self.assertIn(self.wait_deadline_msg, rendered_response)

def test_proctored_exam_attempt_with_past_due_datetime(self):
"""
Test for get_student_view for proctored exam with past due datetime
Expand Down
42 changes: 42 additions & 0 deletions edx_proctoring/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.conf import settings
from django.utils.translation import ugettext as _

from edx_when import api as when_api
from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError

Expand Down Expand Up @@ -231,3 +232,44 @@ def obscured_user_id(user_id, *extra):
obs_hash.update(six.text_type(user_id))
obs_hash.update(u''.join(six.text_type(ext) for ext in extra))
return obs_hash.hexdigest()


def has_due_date_passed(due_datetime):
"""
return True if due date is lesser than current datetime, otherwise False
and if due_datetime is None then we don't have to consider the due date for return False
"""

if due_datetime:
return due_datetime <= datetime.now(pytz.UTC)
return False


def get_exam_due_date(exam, user=None):
"""
Return the due date for the exam.
Uses edx_when to lookup the date for the subsection.
"""
due_date = when_api.get_date_for_block(exam['course_id'], exam['content_id'], 'due', user=user)
return due_date or exam['due_date']


def verify_and_add_wait_deadline(context, exam, user_id):
"""
Verify if the wait deadline should be added to template context.
If the grace period is present and is valid after the exam
has passed due date, for the given user, add the wait deadline to context. If the due
date is not present, which happens for self-paced courses, no context
update will take place.
"""
exam_due_date = get_exam_due_date(exam, user_id)
if not (context.get('grace_period', False) and exam_due_date):
return False
wait_deadline = exam_due_date + context['grace_period']
if not has_due_date_passed(wait_deadline):
context.update(
{'wait_deadline': wait_deadline.isoformat()}
)
return True
return False
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": "2.0.7",
"version": "2.0.8",
"main": "edx_proctoring/static/index.js",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#
appdirs==1.4.3 # via fs
backports.os==0.1.1 # via fs
certifi==2019.6.16 # via requests
certifi==2019.9.11 # via requests
chardet==3.0.4 # via requests
django-crum==0.7.3
django-ipware==2.1.0
Expand Down
6 changes: 3 additions & 3 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ backports.functools-lru-cache==1.5 # via astroid, caniusepython3, isort, pylint
backports.os==0.1.1 # via path.py
bleach==3.1.0 # via readme-renderer
caniusepython3==7.1.0
certifi==2019.6.16 # via requests
certifi==2019.9.11 # via requests
chardet==3.0.4 # via requests
click-log==0.1.8 # via edx-lint
click==7.0 # via click-log, edx-lint, pip-tools
configparser==4.0.1 # via importlib-metadata, pydocstyle, pylint
configparser==4.0.2 # via importlib-metadata, pydocstyle, pylint
contextlib2==0.5.5 # via importlib-metadata
diff-cover==2.3.0
distlib==0.2.9.post0 # via caniusepython3
Expand All @@ -28,7 +28,7 @@ filelock==3.0.12 # via tox
future==0.17.1 # via backports.os
futures==3.3.0 ; python_version == "2.7" # via caniusepython3, isort
idna==2.8 # via requests
importlib-metadata==0.21 # via path.py, pluggy, tox
importlib-metadata==0.22 # via path.py, pluggy, tox
inflect==2.1.0 # via jinja2-pluralize
isort==4.3.21
jinja2-pluralize==0.3.0 # via diff-cover
Expand Down
2 changes: 1 addition & 1 deletion requirements/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ attrs==19.1.0 # via packaging
babel==2.7.0 # via sphinx
backports.os==0.1.1 # via fs
bleach==3.1.0 # via readme-renderer
certifi==2019.6.16 # via requests
certifi==2019.9.11 # via requests
chardet==3.0.4 # via doc8, requests
django-crum==0.7.3
django-ipware==2.1.0
Expand Down
Loading

0 comments on commit a179233

Please sign in to comment.