Skip to content

Commit

Permalink
Merge pull request #498 from edx/matthugs/block-starting-of-second-pr…
Browse files Browse the repository at this point in the history
…octored-session

Block starting second proctored session
  • Loading branch information
matthugs authored Jan 8, 2019
2 parents 8cf9f55 + c8a09ce commit 1c1dffb
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 86 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__ = '1.5.1'
__version__ = '1.5.2'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
218 changes: 134 additions & 84 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1241,8 +1241,8 @@ def get_last_exam_completion_date(course_id, username):
def get_active_exams_for_user(user_id, course_id=None):
"""
This method will return a list of active exams for the user,
i.e. started_at != None and completed_at == None. Theoretically there
could be more than one, but in practice it will be one active exam.
i.e. started_at != None and completed_at == None. There will only
be one.
If course_id is set, then attempts only for an exam in that course_id
should be returned.
Expand Down Expand Up @@ -1602,7 +1602,12 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
if has_due_date_passed(exam['due_date']):
student_view_template = 'timed_exam/expired.html'
else:
student_view_template = 'timed_exam/entrance.html'
is_other_exam, other_exam_url = _get_running_exam_info(user_id, exam_id)
if is_other_exam:
context.update({'exam_url': other_exam_url})
student_view_template = 'proctored_exam/other_exam_in_progress.html'
else:
student_view_template = 'timed_exam/entrance.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.started:
# when we're taking the exam we should not override the view
return None
Expand All @@ -1611,7 +1616,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
# If we are not hiding the exam after the due_date has passed,
# check if the exam's due_date has passed. If so, return None
# so that the user can see his exam answers in read only mode.
# so that the user can see their exam answers in read only mode.
if not exam['hide_after_due'] and has_due_date_passed(exam['due_date']):
return None

Expand Down Expand Up @@ -1745,22 +1750,35 @@ def _get_practice_exam_view(exam, context, exam_id, user_id, course_id):
attempt_status = attempt['status'] if attempt else None
provider = get_backend_provider(exam)

if not attempt_status:
student_view_template = 'practice_exam/entrance.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.started:
if attempt_status == ProctoredExamStudentAttemptStatus.started:
# when we're taking the exam we should not override the view
return None
elif attempt_status in [ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked]:
provider_attempt = provider.get_attempt(attempt)
student_view_template = 'proctored_exam/instructions.html'
context.update({
'exam_code': attempt['attempt_code'],
'backend_instructions': provider_attempt.get('instructions', None),
'software_download_url': provider_attempt.get('download_url', None) or provider.get_software_download_url(),
})
elif attempt_status == ProctoredExamStudentAttemptStatus.ready_to_start:
student_view_template = 'proctored_exam/ready_to_start.html'
elif not attempt_status or attempt_status in [
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
ProctoredExamStudentAttemptStatus.ready_to_start,
]:
is_other_exam, other_exam_url = _get_running_exam_info(user_id, exam_id)
if is_other_exam:
context.update({'exam_url': other_exam_url})
student_view_template = 'proctored_exam/other_exam_in_progress.html'
elif not attempt_status:
student_view_template = 'practice_exam/entrance.html'
elif attempt_status in [
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
]:
provider_attempt = provider.get_attempt(attempt)
student_view_template = 'proctored_exam/instructions.html'
context.update({
'exam_code': attempt['attempt_code'],
'backend_instructions': provider_attempt.get('instructions', None),
'software_download_url': (provider_attempt.get('download_url', None)
or provider.get_software_download_url()),
})
else:
# note: then the status must be ready_to_start
student_view_template = 'proctored_exam/ready_to_start.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.error:
student_view_template = 'practice_exam/error.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
Expand Down Expand Up @@ -1807,76 +1825,86 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):

provider = get_backend_provider(exam)

if not attempt_status:
# student has not started an attempt
# so, show them:
# 1) If there are failed prerequisites then block user and say why
# 2) If there are pending prerequisites then block user and allow them to remediate them
# 3) If there are declined prerequisites, then we auto-decline proctoring since user
# explicitly declined their interest in credit
# 4) Otherwise - all prerequisites are satisfied - then give user
# option to take exam as proctored

# get information about prerequisites

credit_requirement_status = (
credit_state.get('credit_requirement_status')
if credit_state else []
)

prerequisite_status = _are_prerequirements_satisfied(
credit_requirement_status,
evaluate_for_requirement_name=exam['content_id'],
filter_out_namespaces=['grade']
)
if attempt_status == ProctoredExamStudentAttemptStatus.started:
# when we're taking the exam we should not override the view
return None
elif not attempt_status or attempt_status in [
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
ProctoredExamStudentAttemptStatus.ready_to_start,
]:
is_other_exam, other_exam_url = _get_running_exam_info(user_id, exam_id)
if is_other_exam:
context.update({'exam_url': other_exam_url})
student_view_template = 'proctored_exam/other_exam_in_progress.html'
elif not attempt_status:
# student has not started an attempt
# so, show them:
# 1) If there are failed prerequisites then block user and say why
# 2) If there are pending prerequisites then block user and allow them to remediate them
# 3) If there are declined prerequisites, then we auto-decline proctoring since user
# explicitly declined their interest in credit
# 4) Otherwise - all prerequisites are satisfied - then give user
# option to take exam as proctored

# get information about prerequisites

credit_requirement_status = (
credit_state.get('credit_requirement_status')
if credit_state else []
)

# add any prerequisite information, if applicable
context.update({
'prerequisite_status': prerequisite_status
})
prerequisite_status = _are_prerequirements_satisfied(
credit_requirement_status,
evaluate_for_requirement_name=exam['content_id'],
filter_out_namespaces=['grade']
)

# if exam due date has passed, then we can't take the exam
if has_due_date_passed(exam['due_date']):
student_view_template = 'proctored_exam/expired.html'
elif not prerequisite_status['are_prerequisites_satisifed']:
# do we have any declined prerequisites, if so, then we
# will auto-decline this proctored exam
if prerequisite_status['declined_prerequisites']:
# user hasn't a record of attempt, create one now
# so we can mark it as declined
_create_and_decline_attempt(exam_id, user_id)
return None
# add any prerequisite information, if applicable
context.update({
'prerequisite_status': prerequisite_status
})

# do we have failed prerequisites? That takes priority in terms of
# messaging
if prerequisite_status['failed_prerequisites']:
# Let's resolve the URLs to jump to this prequisite
prerequisite_status['failed_prerequisites'] = _resolve_prerequisite_links(
exam,
prerequisite_status['failed_prerequisites']
)
student_view_template = 'proctored_exam/failed-prerequisites.html'
# if exam due date has passed, then we can't take the exam
if has_due_date_passed(exam['due_date']):
student_view_template = 'proctored_exam/expired.html'
elif not prerequisite_status['are_prerequisites_satisifed']:
# do we have any declined prerequisites, if so, then we
# will auto-decline this proctored exam
if prerequisite_status['declined_prerequisites']:
# user hasn't a record of attempt, create one now
# so we can mark it as declined
_create_and_decline_attempt(exam_id, user_id)
return None

# do we have failed prerequisites? That takes priority in terms of
# messaging
if prerequisite_status['failed_prerequisites']:
# Let's resolve the URLs to jump to this prequisite
prerequisite_status['failed_prerequisites'] = _resolve_prerequisite_links(
exam,
prerequisite_status['failed_prerequisites']
)
student_view_template = 'proctored_exam/failed-prerequisites.html'
else:
# Let's resolve the URLs to jump to this prequisite
prerequisite_status['pending_prerequisites'] = _resolve_prerequisite_links(
exam,
prerequisite_status['pending_prerequisites']
)
student_view_template = 'proctored_exam/pending-prerequisites.html'
else:
# Let's resolve the URLs to jump to this prequisite
prerequisite_status['pending_prerequisites'] = _resolve_prerequisite_links(
exam,
prerequisite_status['pending_prerequisites']
)
student_view_template = 'proctored_exam/pending-prerequisites.html'
else:
student_view_template = 'proctored_exam/entrance.html'
# emit an event that the user was presented with the option
# to start timed exam
emit_event(exam, 'option-presented')
elif attempt_status == ProctoredExamStudentAttemptStatus.started:
# when we're taking the exam we should not override the view
return None
elif attempt_status in [ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked]:
if context.get('verification_status') is not APPROVED_STATUS:
student_view_template = 'proctored_exam/entrance.html'
# emit an event that the user was presented with the option
# to start timed exam
emit_event(exam, 'option-presented')
elif context.get('verification_status') is not APPROVED_STATUS:
# if the user has not id verified yet, show them the page that requires them to do so
student_view_template = 'proctored_exam/id_verification.html'
else:
elif attempt_status in [
ProctoredExamStudentAttemptStatus.created,
ProctoredExamStudentAttemptStatus.download_software_clicked,
]:
provider_attempt = provider.get_attempt(attempt)
student_view_template = 'proctored_exam/instructions.html'
download_url = provider_attempt.get('download_url', None) or provider.get_software_download_url()
Expand All @@ -1885,8 +1913,9 @@ def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
'backend_instructions': provider_attempt.get('instructions', None),
'software_download_url': download_url
})
elif attempt_status == ProctoredExamStudentAttemptStatus.ready_to_start:
student_view_template = 'proctored_exam/ready_to_start.html'
else:
# note: then the status must be ready_to_start
student_view_template = 'proctored_exam/ready_to_start.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.error:
student_view_template = 'proctored_exam/error.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.timed_out:
Expand Down Expand Up @@ -1995,6 +2024,27 @@ def get_student_view(user_id, course_id, content_id,
return None


def _get_running_exam_info(user_id, currently_visited_exam_id):
"""
Check whether there are any other currently active exams besides
that currently being viewed
"""
active_exam_attempts = get_active_exams_for_user(user_id)
if active_exam_attempts:
active_exam = active_exam_attempts[0]['exam']
is_other_exam_running = active_exam['id'] != currently_visited_exam_id
try:
# resolve the LMS url, note we can't assume we're running in
# a same process as the LMS
other_exam_url = reverse('jump_to', args=[active_exam['course_id'], active_exam['content_id']])
except NoReverseMatch:
log.exception("Can't find exam url for course %s", active_exam['course_id'])
other_exam_url = ''
else:
is_other_exam_running, other_exam_url = False, ''
return is_other_exam_running, other_exam_url


def get_exam_violation_report(course_id, include_practice_exams=False):
"""
Returns proctored exam attempts for the course id, including review details.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% load i18n %}
<div class="failure sequence proctored-exam" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
Complete in-progress exam
{% endblocktrans %}
</h3>

<p>
{% blocktrans %}
You must <a href="{{ exam_url }}">complete your currently in-progress exam</a> before you can attempt this one.
{% endblocktrans %}
</p>
</div>
{% include 'proctored_exam/footer.html' %}
53 changes: 53 additions & 0 deletions edx_proctoring/tests/test_student_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def setUp(self):
self.practice_exam_submitted_msg = 'You have submitted this practice proctored exam'
self.take_exam_without_proctoring_msg = 'Take this exam without proctoring'
self.ready_to_start_msg = 'Important'
self.complete_other_exam_first_msg = 'Complete in-progress exam'
self.footer_msg = 'About Proctored Exams'
self.timed_footer_msg = 'Can I request additional time to complete my exam?'

Expand Down Expand Up @@ -131,6 +132,18 @@ def render_practice_exam(self, context_overrides=None):
context_overrides=exam_context_overrides
)

def render_timed_exam(self):
"""
Renders a test timed exam
"""
exam_context_overrides = {
'is_proctored': False
}
return self._render_exam(
self.timed_exam_id,
context_overrides=exam_context_overrides
)

def test_get_student_view(self):
"""
Test for get_student_view prompting the user to take the exam
Expand Down Expand Up @@ -457,6 +470,46 @@ def test_declined_attempt(self):
rendered_response = self.render_proctored_exam()
self.assertIsNone(rendered_response)

def test_get_studentview_exam_in_progress(self):
"""
Assert that we get the right content when another exam was
started first
"""
self._create_started_exam_attempt()
self.content_id = self.content_id + '_new'
self.proctored_exam_id = self._create_proctored_exam()
unstarted_attempt = self._create_unstarted_exam_attempt()

unstarted_attempt.status = ProctoredExamStudentAttemptStatus.created
unstarted_attempt.save()

rendered_response = self.render_proctored_exam()
self.assertIn(self.complete_other_exam_first_msg, rendered_response)

def test_get_studentview_exam_in_progress_timed(self):
"""
Assert that we get the right content when another exam was
started first for timed and practice exams
"""
self._create_started_exam_attempt()

rendered_response = self.render_timed_exam()
self.assertIn(self.complete_other_exam_first_msg, rendered_response)

def test_get_studentview_exam_in_progress_practice(self):
"""
Assert that we get the right content when another exam was
started first for timed and practice exams
"""
self._create_started_exam_attempt()
unstarted_attempt = self._create_unstarted_exam_attempt(is_practice=True)

unstarted_attempt.status = ProctoredExamStudentAttemptStatus.created
unstarted_attempt.save()

rendered_response = self.render_practice_exam()
self.assertIn(self.complete_other_exam_first_msg, rendered_response)

def test_get_studentview_ready(self):
"""
Assert that we get the right content
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": "1.5.1",
"version": "1.5.2",
"main": "edx_proctoring/static/index.js",
"repository": {
"type": "git",
Expand Down

0 comments on commit 1c1dffb

Please sign in to comment.