Skip to content

Commit

Permalink
Merge pull request #984 from edx/alangsto/timed_exams_should_be_visible
Browse files Browse the repository at this point in the history
fix: timed exams should still be visible after course end date
  • Loading branch information
alangsto authored Oct 26, 2021
2 parents 058c9ab + 24199b4 commit b2f7932
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 10 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
~~~~~~~~~~

[4.2.0] - 2021-10-20
~~~~~~~~~~~~~~~~~~~~
* Timed exams should remain visible after the course end date has passed

[4.1.3] - 2021-10-15
~~~~~~~~~~~~~~~~~~~~
* Always allow practice attempts to trigger grade/credit/certificate updates
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__ = '4.1.3'
__version__ = '4.2.0'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
9 changes: 8 additions & 1 deletion edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
get_time_remaining_for_attempt,
get_user_course_outline_details,
has_due_date_passed,
has_end_date_passed,
humanized_time,
is_reattempting_exam,
obscured_user_id,
Expand Down Expand Up @@ -881,7 +882,13 @@ def is_exam_passed_due(exam, user=None):
Return whether the due date has passed.
Uses edx_when to lookup the date for the subsection.
"""
return has_due_date_passed(get_exam_due_date(exam, user=user))
return (
has_due_date_passed(get_exam_due_date(exam, user=user))
# if the exam is timed and passed the course end date, it should also be considered passed due
or (
not exam['is_proctored'] and not exam['is_practice_exam'] and has_end_date_passed(exam['course_id'])
)
)


def _was_review_status_acknowledged(is_status_acknowledged, exam):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def setUp(self):
set_runtime_service('grades', MockGradesService())
set_runtime_service('certificates', MockCertificateService())
self.exam_id = create_exam(
course_id='foo',
course_id='a/b/c',
content_id='bar',
exam_name='Test Exam',
time_limit_mins=90
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ def setUp(self):
set_runtime_service('grades', MockGradesService())
set_runtime_service('certificates', MockCertificateService())
self.first_exam_id = create_exam(
course_id='foo',
course_id='a/b/c',
content_id='bar',
exam_name='Test Exam 1',
time_limit_mins=90
)
self.second_exam_id = create_exam(
course_id='foo',
course_id='a/b/c',
content_id='baz',
exam_name='Test Exam 2',
time_limit_mins=90
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_run_command(self):
Run the management command
"""
exam_id = create_exam(
course_id='foo',
course_id='a/b/c',
content_id='bar',
exam_name='Test Exam 1',
time_limit_mins=90
Expand Down
79 changes: 78 additions & 1 deletion edx_proctoring/tests/test_student_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,83 @@ def test_get_studentview_submitted_timed_exam_with_past_due_date(self, due_date,
else:
self.assertIsNone(rendered_response)

@ddt.data(
datetime(2019, 4, 4).replace(tzinfo=pytz.UTC),
datetime.now(pytz.UTC) + timedelta(days=2),
datetime(9999, 4, 4).replace(tzinfo=pytz.UTC),
'bad'
)
@patch('edx_when.api.get_dates_for_course')
def test_get_studentview_submitted_timed_exam_past_course_end(self, end_date, mock_course_dates):
"""
Test get_student_view timed exam returns None if we are passed the course end date
This is to fix inconsistent behavior for timed exam visibility in self paced courses,
where timed exams would be visible after a self paced due date, but not visible after
the course end date. Timed exams in instructor-paced courses are always visible after
the exam due date, and therefore after the course end date.
"""
dates = [
(('11111111', 'start'), datetime(2019, 3, 15).replace(tzinfo=pytz.UTC)),
(('22222222', 'due'), datetime(2019, 3, 28).replace(tzinfo=pytz.UTC)),
(('33333333', 'end'), end_date),
]
mock_course_dates.return_value = dict(dates)

self._create_exam_attempt(self.timed_exam_id, status='submitted')

rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': 10,
}
)

if not isinstance(end_date, str) and end_date < datetime.now(pytz.UTC):
self.assertIsNone(rendered_response)
else:
self.assertIn(self.timed_exam_submitted, rendered_response)

@ddt.data(
True,
False
)
@patch('edx_when.api.get_dates_for_course')
def test_get_studentview_submitted_timed_exam_past_course_end_no_date(self, include_end, mock_course_dates):
"""
Test get_student_view timed exam returns blocks learners if no end date is specified
This is to fix inconsistent behavior for timed exam visibility in self paced courses,
where timed exams would be visible after a self paced due date, but not visible after
the course end date. Timed exams in instructor-paced courses are always visible after
the exam due date, and therefore after the course end date.
"""
dates = [
(('11111111', 'start'), datetime(2019, 3, 15).replace(tzinfo=pytz.UTC)),
(('22222222', 'due'), datetime(2019, 3, 28).replace(tzinfo=pytz.UTC)),
]
if include_end:
dates.append((('33333333', 'end'), None))

mock_course_dates.return_value = dict(dates)

self._create_exam_attempt(self.timed_exam_id, status='submitted')

rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': 10,
}
)

self.assertIn(self.timed_exam_submitted, rendered_response)

@ddt.data(
(False, 'submitted', True, 1),
(True, 'verified', False, 1),
Expand Down Expand Up @@ -1291,7 +1368,7 @@ def test_get_studentview_unstarted_timed_exam(self):
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id="abc",
course_id='d/e/f',
content_id=self.content_id,
context={
'is_proctored': False,
Expand Down
25 changes: 23 additions & 2 deletions edx_proctoring/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import hashlib
import hmac
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone

import pytz
from cryptography.hazmat.backends import default_backend
Expand Down Expand Up @@ -244,12 +244,33 @@ 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_course_end_date(course_id):
"""
Return the end date for the given course id
"""
end_date = None
dates = when_api.get_dates_for_course(course_id)
end_dates = list(filter(lambda elem: elem[0][1] == 'end', dates.items()))
if end_dates and end_dates[0][1]:
try:
end_date = end_dates[0][1].replace(tzinfo=timezone.utc)
except (AttributeError, TypeError):
log.error('Could not retrieve course end date for course_id=%(course_id)s', {'course_id': course_id})
return end_date


def has_end_date_passed(course_id):
"""
Return True if the course end date has passed, otherwise False
"""
return has_due_date_passed(get_course_end_date(course_id))


def get_exam_due_date(exam, user=None):
"""
Return the due date for the exam.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@edx/edx-proctoring",
"//": "Note that the version format is slightly different than that of the Python version when using prereleases.",
"version": "4.1.3",
"version": "4.2.0",
"main": "edx_proctoring/static/index.js",
"scripts": {
"test": "gulp test"
Expand Down

0 comments on commit b2f7932

Please sign in to comment.