Commit baaaafef by Afzal Wali

Separating the views for proctored, practice and timed exams.

Addressed the suggestions

concatenation with format fucntion

Removed some unused context variables for timed exams to reduce complexity.
Made a common context dictionary for Practice and Proctored exams.

Addressing a bug. Added a testcase.

bug fix.

bug fix.

Updated the docstrings.
parent 3c36cc69
...@@ -1016,46 +1016,171 @@ def get_attempt_status_summary(user_id, course_id, content_id): ...@@ -1016,46 +1016,171 @@ def get_attempt_status_summary(user_id, course_id, content_id):
return summary return summary
def get_student_view(user_id, course_id, content_id, def _does_time_remain(attempt):
context, user_role='student'):
""" """
Helper method that will return the view HTML related to the exam control Helper function returns True if time remains for an attempt and False
flow (i.e. entering, expired, completed, etc.) If there is no specific otherwise. Called from _get_timed_exam_view(), _get_practice_exam_view()
content to display, then None will be returned and the caller should and _get_proctored_exam_view()
render it's own view
""" """
does_time_remain = False
has_started_exam = (
attempt and
attempt.get('started_at') and
ProctoredExamStudentAttemptStatus.is_incomplete_status(attempt.get('status'))
)
if has_started_exam:
expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins'])
does_time_remain = datetime.now(pytz.UTC) < expires_at
return does_time_remain
# non-student roles should never see any proctoring related
# screens
if user_role != 'student':
return None
def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
"""
Returns a rendered view for the Timed Exams
"""
student_view_template = None student_view_template = None
attempt = get_exam_attempt(exam_id, user_id)
exam_id = None attempt_status = attempt['status'] if attempt else None
try:
exam = get_exam_by_content_id(course_id, content_id)
if not exam['is_active']:
# Exam is no longer active
# Note, we don't hard delete exams since we need to retain
# data
return None
exam_id = exam['id'] if not attempt_status:
except ProctoredExamNotFoundException: student_view_template = 'timed_exam/entrance.html'
# This really shouldn't happen elif attempt_status == ProctoredExamStudentAttemptStatus.started:
# as Studio will be setting this up # when we're taking the exam we should override the view
exam_id = create_exam( return None
course_id=course_id, elif attempt_status == ProctoredExamStudentAttemptStatus.ready_to_submit:
content_id=unicode(content_id), student_view_template = 'timed_exam/ready_to_submit.html'
exam_name=context['display_name'], elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
time_limit_mins=context['default_time_limit_mins'], student_view_template = 'timed_exam/submitted.html'
is_proctored=context.get('is_proctored', False),
is_practice_exam=context.get('is_practice_exam', False) if student_view_template:
template = loader.get_template(student_view_template)
django_context = Context(context)
allowed_time_limit_mins = attempt['allowed_time_limit_mins'] if attempt else None
if not allowed_time_limit_mins:
# no existing attempt, so compute the user's allowed
# time limit, including any accommodations
allowed_time_limit_mins = exam['time_limit_mins']
allowance = ProctoredExamStudentAllowance.get_allowance_for_user(
exam_id,
user_id,
"Additional time (minutes)"
)
if allowance:
allowed_time_limit_mins += int(allowance.value)
total_time = humanized_time(allowed_time_limit_mins)
progress_page_url = ''
try:
progress_page_url = reverse(
'courseware.views.progress',
args=[course_id]
)
except NoReverseMatch:
# we are allowing a failure here since we can't guarantee
# that we are running in-proc with the edx-platform LMS
# (for example unit tests)
pass
django_context.update({
'total_time': total_time,
'exam_id': exam_id,
'progress_page_url': progress_page_url,
'does_time_remain': _does_time_remain(attempt),
'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
'change_state_url': reverse(
'edx_proctoring.proctored_exam.attempt',
args=[attempt['id']]
) if attempt else '',
})
return template.render(django_context)
def _get_proctored_exam_context(exam, attempt, course_id, is_practice_exam=False):
"""
Common context variables for the Proctored and Practice exams' templates.
"""
attempt_time = attempt['allowed_time_limit_mins'] if attempt else exam['time_limit_mins']
total_time = humanized_time(attempt_time)
progress_page_url = ''
try:
progress_page_url = reverse(
'courseware.views.progress',
args=[course_id]
) )
exam = get_exam_by_content_id(course_id, content_id) except NoReverseMatch:
# we are allowing a failure here since we can't guarantee
# that we are running in-proc with the edx-platform LMS
# (for example unit tests)
pass
return {
'platform_name': settings.PLATFORM_NAME,
'total_time': total_time,
'exam_id': exam['id'],
'progress_page_url': progress_page_url,
'is_sample_attempt': is_practice_exam,
'does_time_remain': _does_time_remain(attempt),
'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
'exam_started_poll_url': reverse(
'edx_proctoring.proctored_exam.attempt',
args=[attempt['id']]
) if attempt else '',
'change_state_url': reverse(
'edx_proctoring.proctored_exam.attempt',
args=[attempt['id']]
) if attempt else '',
'link_urls': settings.PROCTORING_SETTINGS.get('LINK_URLS', {}),
}
def _get_practice_exam_view(exam, context, exam_id, user_id, course_id):
"""
Returns a rendered view for the practice Exams
"""
student_view_template = None
attempt = get_exam_attempt(exam_id, user_id)
is_proctored = exam['is_proctored'] attempt_status = attempt['status'] if attempt else None
if not attempt_status:
student_view_template = 'practice_exam/entrance.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.started:
# when we're taking the exam we should override the view
return None
elif attempt_status == ProctoredExamStudentAttemptStatus.created:
provider = get_backend_provider()
student_view_template = 'proctored_exam/instructions.html'
context.update({
'exam_code': attempt['attempt_code'],
'software_download_url': provider.get_software_download_url(),
})
elif attempt_status == ProctoredExamStudentAttemptStatus.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:
student_view_template = 'practice_exam/submitted.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.ready_to_submit:
student_view_template = 'proctored_exam/ready_to_submit.html'
if student_view_template:
template = loader.get_template(student_view_template)
django_context = Context(context)
django_context.update(_get_proctored_exam_context(exam, attempt, course_id, is_practice_exam=True))
return template.render(django_context)
def _get_proctored_exam_view(exam, context, exam_id, user_id, course_id):
"""
Returns a rendered view for the Proctored Exams
"""
student_view_template = None
# see if only 'verified' track students should see this *except* if it is a practice exam # see if only 'verified' track students should see this *except* if it is a practice exam
check_mode_and_eligibility = ( check_mode_and_eligibility = (
...@@ -1095,98 +1220,95 @@ def get_student_view(user_id, course_id, content_id, ...@@ -1095,98 +1220,95 @@ def get_student_view(user_id, course_id, content_id,
# don't override context, let the courseware show # don't override context, let the courseware show
return None return None
attempt = get_exam_attempt(exam_id, user_id) attempt = get_exam_attempt(exam_id, user_id)
attempt_status = attempt['status'] if attempt else None
# if user has declined the attempt, then we don't show the # if user has declined the attempt, then we don't show the
# proctored exam # proctored exam
if attempt and attempt['status'] == ProctoredExamStudentAttemptStatus.declined: if attempt_status == ProctoredExamStudentAttemptStatus.declined:
return None return None
does_time_remain = False if not attempt_status:
has_started_exam = attempt and attempt.get('started_at') student_view_template = 'proctored_exam/entrance.html'
if has_started_exam: elif attempt_status == ProctoredExamStudentAttemptStatus.started:
expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins']) # when we're taking the exam we should override the view
does_time_remain = datetime.now(pytz.UTC) < expires_at return None
elif attempt_status == ProctoredExamStudentAttemptStatus.created:
if not attempt:
# determine whether to show a timed exam only entrance screen
# or a screen regarding proctoring
if is_proctored:
if exam['is_practice_exam']:
student_view_template = 'proctoring/seq_proctored_practice_exam_entrance.html'
else:
student_view_template = 'proctoring/seq_proctored_exam_entrance.html'
else:
student_view_template = 'proctoring/seq_timed_exam_entrance.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.created:
provider = get_backend_provider() provider = get_backend_provider()
student_view_template = 'proctoring/seq_proctored_exam_instructions.html' student_view_template = 'proctored_exam/instructions.html'
context.update({ context.update({
'exam_code': attempt['attempt_code'], 'exam_code': attempt['attempt_code'],
'software_download_url': provider.get_software_download_url(), 'software_download_url': provider.get_software_download_url(),
}) })
elif attempt['status'] == ProctoredExamStudentAttemptStatus.ready_to_start: elif attempt_status == ProctoredExamStudentAttemptStatus.ready_to_start:
student_view_template = 'proctoring/seq_proctored_exam_ready_to_start.html' student_view_template = 'proctored_exam/ready_to_start.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.error: elif attempt_status == ProctoredExamStudentAttemptStatus.error:
if attempt['is_sample_attempt']: student_view_template = 'proctored_exam/error.html'
student_view_template = 'proctoring/seq_proctored_practice_exam_error.html' elif attempt_status == ProctoredExamStudentAttemptStatus.timed_out:
else: raise NotImplementedError('There is no defined rendering for ProctoredExamStudentAttemptStatus.timed_out!')
student_view_template = 'proctoring/seq_proctored_exam_error.html' elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
elif attempt['status'] == ProctoredExamStudentAttemptStatus.timed_out: student_view_template = 'proctored_exam/submitted.html'
student_view_template = 'proctoring/seq_timed_exam_expired.html' elif attempt_status == ProctoredExamStudentAttemptStatus.verified:
elif attempt['status'] == ProctoredExamStudentAttemptStatus.submitted: student_view_template = 'proctored_exam/verified.html'
if attempt['is_sample_attempt']: elif attempt_status == ProctoredExamStudentAttemptStatus.rejected:
student_view_template = 'proctoring/seq_proctored_practice_exam_submitted.html' student_view_template = 'proctored_exam/rejected.html'
else: elif attempt_status == ProctoredExamStudentAttemptStatus.ready_to_submit:
student_view_template = 'proctoring/seq_proctored_exam_submitted.html' student_view_template = 'proctored_exam/ready_to_submit.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.verified:
student_view_template = 'proctoring/seq_proctored_exam_verified.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.rejected:
student_view_template = 'proctoring/seq_proctored_exam_rejected.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.ready_to_submit:
if is_proctored:
student_view_template = 'proctoring/seq_proctored_exam_ready_to_submit.html'
else:
student_view_template = 'proctoring/seq_timed_exam_ready_to_submit.html'
if student_view_template: if student_view_template:
template = loader.get_template(student_view_template) template = loader.get_template(student_view_template)
django_context = Context(context) django_context = Context(context)
attempt_time = attempt['allowed_time_limit_mins'] if attempt else exam['time_limit_mins'] django_context.update(_get_proctored_exam_context(exam, attempt, course_id))
total_time = humanized_time(attempt_time)
progress_page_url = ''
try:
progress_page_url = reverse(
'courseware.views.progress',
args=[course_id]
)
except NoReverseMatch:
# we are allowing a failure here since we can't guarantee
# that we are running in-proc with the edx-platform LMS
# (for example unit tests)
pass
django_context.update({
'platform_name': settings.PLATFORM_NAME,
'total_time': total_time,
'exam_id': exam_id,
'progress_page_url': progress_page_url,
'is_sample_attempt': attempt['is_sample_attempt'] if attempt else False,
'does_time_remain': does_time_remain,
'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
'exam_started_poll_url': reverse(
'edx_proctoring.proctored_exam.attempt',
args=[attempt['id']]
) if attempt else '',
'change_state_url': reverse(
'edx_proctoring.proctored_exam.attempt',
args=[attempt['id']]
) if attempt else '',
'link_urls': settings.PROCTORING_SETTINGS.get('LINK_URLS', {}),
})
return template.render(django_context) return template.render(django_context)
def get_student_view(user_id, course_id, content_id,
context, user_role='student'):
"""
Helper method that will return the view HTML related to the exam control
flow (i.e. entering, expired, completed, etc.) If there is no specific
content to display, then None will be returned and the caller should
render it's own view
"""
# non-student roles should never see any proctoring related
# screens
if user_role != 'student':
return None
exam_id = None
try:
exam = get_exam_by_content_id(course_id, content_id)
if not exam['is_active']:
# Exam is no longer active
# Note, we don't hard delete exams since we need to retain
# data
return None
exam_id = exam['id']
except ProctoredExamNotFoundException:
# This really shouldn't happen
# as Studio will be setting this up
exam_id = create_exam(
course_id=course_id,
content_id=unicode(content_id),
exam_name=context['display_name'],
time_limit_mins=context['default_time_limit_mins'],
is_proctored=context.get('is_proctored', False),
is_practice_exam=context.get('is_practice_exam', False)
)
exam = get_exam_by_content_id(course_id, content_id)
is_practice_exam = exam['is_proctored'] and exam['is_practice_exam']
is_proctored_exam = exam['is_proctored'] and not exam['is_practice_exam']
is_timed_exam = not exam['is_proctored'] and not exam['is_practice_exam']
if is_timed_exam:
return _get_timed_exam_view(exam, context, exam_id, user_id, course_id)
elif is_practice_exam:
return _get_practice_exam_view(exam, context, exam_id, user_id, course_id)
elif is_proctored_exam:
return _get_proctored_exam_view(exam, context, exam_id, user_id, course_id)
return None return None
...@@ -50,7 +50,7 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume ...@@ -50,7 +50,7 @@ def start_exam_callback(request, attempt_code): # pylint: disable=unused-argume
mark_exam_attempt_as_ready(attempt['proctored_exam']['id'], attempt['user']['id']) mark_exam_attempt_as_ready(attempt['proctored_exam']['id'], attempt['user']['id'])
template = loader.get_template('proctoring/proctoring_launch_callback.html') template = loader.get_template('proctored_exam/proctoring_launch_callback.html')
poll_url = reverse( poll_url = reverse(
'edx_proctoring.anonymous.proctoring_poll_status', 'edx_proctoring.anonymous.proctoring_poll_status',
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<i class="fa fa-arrow-circle-right start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false></i> <i class="fa fa-arrow-circle-right start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false></i>
</button> </button>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/footer.html' %}
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
</a> </a>
</button> </button>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/footer.html' %}
<script type="text/javascript"> <script type="text/javascript">
$('.start-timed-exam').click( $('.start-timed-exam').click(
function(event) { function(event) {
......
"""
We need python think this is a python module
"""
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/footer.html' %}
<script type="text/javascript"> <script type="text/javascript">
var startProctoredExam = function(selector, exam_id, action_url, start_immediately, attempt_proctored) { var startProctoredExam = function(selector, exam_id, action_url, start_immediately, attempt_proctored) {
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
alert(msg); alert(msg);
}); });
}; };
var inProcess = false; var inProcess = false;
var disableClickEvent = function (selector) { var disableClickEvent = function (selector) {
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
$( this ).css('cursor', 'pointer'); $( this ).css('cursor', 'pointer');
}); });
}; };
$('.start-timed-exam').click( $('.start-timed-exam').click(
function() { function() {
if (!inProcess) { if (!inProcess) {
......
...@@ -33,4 +33,4 @@ ...@@ -33,4 +33,4 @@
</p> </p>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/footer.html' %}
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
</p> </p>
{% endif %} {% endif %}
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/footer.html' %}
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/footer.html' %}
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -33,4 +33,4 @@ ...@@ -33,4 +33,4 @@
</p> </p>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/footer.html' %}
...@@ -30,4 +30,4 @@ ...@@ -30,4 +30,4 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/footer.html' %}
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctored_exam/seq_proctored_exam_footer.html' %}
{% load i18n %}
<div class="critical-time sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
You did not complete the exam in the allotted time
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
You are not eligible to receive credit for this exam because you did not submit
your exam responses before time expired. Other work that you have completed in this
course contributes to your final grade. See the <a href="{{progress_page_url}}">Progress page</a>
for your current grade in the course.
{% endblocktrans %}
</p>
<div class="proctored-exam-message">
<p>
{% blocktrans %}
Other work that you have completed in this course contributes to your final grade.
{% endblocktrans %}
</p>
</div>
</div>
{% include 'proctoring/seq_timed_exam_footer.html' %}
"""
We need python think this is a python module
"""
...@@ -12,16 +12,16 @@ ...@@ -12,16 +12,16 @@
</strong> </strong>
{% trans "As soon as you indicate that you are ready to start the exam, you will have "%} {{total_time|lower}} {% trans " to complete the exam." %} {% trans "As soon as you indicate that you are ready to start the exam, you will have "%} {{total_time|lower}} {% trans " to complete the exam." %}
</p> </p>
<div class="gated-sequence"> <button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}">
<a class='start-timed-exam' data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}"> <a>
{% blocktrans %} {% blocktrans %}
I'm ready! Start this timed exam. I'm ready! Start this timed exam.
{% endblocktrans %} {% endblocktrans %}
<i class="fa fa-arrow-circle-right"></i> <i class="fa fa-arrow-circle-right"></i>
</a> </a>
</div> </button>
</div> </div>
{% include 'proctoring/seq_timed_exam_footer.html' %} {% include 'timed_exam/footer.html' %}
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="sequence timed-exam completed" data-exam-id="{{exam_id}}"> <div class="sequence timed-exam completed" data-exam-id="{{exam_id}}">
<h3> <h3>
{% blocktrans %} {% blocktrans %}
This is the end of your timed exam Are you sure you want to end your timed exam?
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<p> <p>
...@@ -11,11 +11,22 @@ ...@@ -11,11 +11,22 @@
and your worked will then be graded. and your worked will then be graded.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<button type="button" name="submit-timed-exam" class="exam-action-button" data-action="submit" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}" > <div>
{% blocktrans %} <button type="button" name="submit-proctored-exam" class="exam-action-button" data-action="submit" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}">
I'm ready! Submit my answers and end my timed exam {% blocktrans %}
{% endblocktrans %} Yes, end my timed exam
</button> {% endblocktrans %}
</button>
</div>
{% if does_time_remain %}
<div>
<button type="button" name="goback-proctored-exam" class="exam-action-button" data-action="start" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}">
{% blocktrans %}
No, I'd like to continue working
{% endblocktrans %}
</button>
</div>
{% endif %}
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$('.exam-action-button').click( $('.exam-action-button').click(
......
{% load i18n %}
<div class="sequence proctored-exam completed" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
You have submitted your timed exam
{% endblocktrans %}
</h3>
<h4>
{% blocktrans %}
Your timed exam status: <b> Completed </b>
{% endblocktrans %}
</h4>
<hr>
<p>
{% blocktrans %}
As you have submitted your exam, you will not be able to re-enter it. Your grades for this timed exam will be immediately available on the <a href="{{progress_page_url}}">Progress</a> page.
{% endblocktrans %}
</p>
</div>
...@@ -95,18 +95,20 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -95,18 +95,20 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.disabled_exam_id = self._create_disabled_exam() self.disabled_exam_id = self._create_disabled_exam()
# Messages for get_student_view # Messages for get_student_view
self.start_an_exam_msg = 'Would you like to take "%s" as a proctored exam?' self.start_an_exam_msg = 'Would you like to take "{exam_name}" as a proctored exam?'
self.timed_exam_msg = '%s is a Timed Exam' self.timed_exam_msg = '{exam_name} is a Timed Exam'
self.exam_time_expired_msg = 'You did not complete the exam in the allotted time' self.exam_time_expired_msg = 'You did not complete the exam in the allotted time'
self.exam_time_error_msg = 'There was a problem with your proctoring session' self.exam_time_error_msg = 'There was a problem with your proctoring session'
self.chose_proctored_exam_msg = 'You Have Chosen To Take a Proctored Exam' self.chose_proctored_exam_msg = 'You Have Chosen To Take a Proctored Exam'
self.proctored_exam_optout_msg = 'Take this exam as an open exam instead'
self.proctored_exam_completed_msg = 'Are you sure you want to end your proctored exam' self.proctored_exam_completed_msg = 'Are you sure you want to end your proctored exam'
self.proctored_exam_submitted_msg = 'You have submitted this proctored exam for review' self.proctored_exam_submitted_msg = 'You have submitted this proctored exam for review'
self.proctored_exam_verified_msg = 'Your proctoring session was reviewed and passed all requirements' self.proctored_exam_verified_msg = 'Your proctoring session was reviewed and passed all requirements'
self.proctored_exam_rejected_msg = 'Your proctoring session was reviewed and did not pass requirements' self.proctored_exam_rejected_msg = 'Your proctoring session was reviewed and did not pass requirements'
self.timed_exam_completed_msg = 'This is the end of your timed exam' self.start_a_practice_exam_msg = 'Would you like to take "{exam_name}" as a practice proctored exam?'
self.start_a_practice_exam_msg = 'Would you like to take "%s" as a practice proctored exam?'
self.practice_exam_submitted_msg = 'You have submitted this practice proctored exam' self.practice_exam_submitted_msg = 'You have submitted this practice proctored exam'
self.practice_exam_created_msg = 'You must set up and start the proctoring software before you begin your exam'
self.practice_exam_completion_msg = 'Are you sure you want to end your proctored exam'
self.ready_to_start_msg = 'Your Proctoring Installation and Set Up is Complete' self.ready_to_start_msg = 'Your Proctoring Installation and Set Up is Complete'
self.practice_exam_failed_msg = 'There was a problem with your practice proctoring session' self.practice_exam_failed_msg = 'There was a problem with your practice proctoring session'
self.proctored_exam_email_subject = 'Proctoring Session Results Update' self.proctored_exam_email_subject = 'Proctoring Session Results Update'
...@@ -163,12 +165,20 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -163,12 +165,20 @@ class ProctoredExamApiTests(LoggedInTestCase):
is_active=False is_active=False
) )
def _create_unstarted_exam_attempt(self, is_proctored=True): def _create_unstarted_exam_attempt(self, is_proctored=True, is_practice=False):
""" """
Creates the ProctoredExamStudentAttempt object. Creates the ProctoredExamStudentAttempt object.
""" """
if is_proctored:
if is_practice:
exam_id = self.practice_exam_id
else:
exam_id = self.proctored_exam_id
else:
exam_id = self.timed_exam
return ProctoredExamStudentAttempt.objects.create( return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.proctored_exam_id if is_proctored else self.timed_exam, proctored_exam_id=exam_id,
user_id=self.user_id, user_id=self.user_id,
external_id=self.external_id, external_id=self.external_id,
allowed_time_limit_mins=10, allowed_time_limit_mins=10,
...@@ -190,7 +200,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -190,7 +200,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
is_sample_attempt=is_sample_attempt is_sample_attempt=is_sample_attempt
) )
def _create_started_practice_exam_attempt(self, started_at=None): # pylint: disable=invalid-name def _create_started_practice_exam_attempt(self, started_at=None):
""" """
Creates the ProctoredExamStudentAttempt object. Creates the ProctoredExamStudentAttempt object.
""" """
...@@ -411,7 +421,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -411,7 +421,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
with self.assertRaises(StudentExamAttemptAlreadyExistsException): with self.assertRaises(StudentExamAttemptAlreadyExistsException):
create_exam_attempt(proctored_exam_student_attempt.proctored_exam.id, self.user_id) create_exam_attempt(proctored_exam_student_attempt.proctored_exam.id, self.user_id)
def test_recreate_a_practice_exam_attempt(self): # pylint: disable=invalid-name def test_recreate_a_practice_exam_attempt(self):
""" """
Taking the practice exam several times should not cause an exception. Taking the practice exam several times should not cause an exception.
""" """
...@@ -644,8 +654,11 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -644,8 +654,11 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90 'default_time_limit_mins': 90
} }
) )
self.assertIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response) self.assertIn(
self.assertIn(self.start_an_exam_msg % self.exam_name, rendered_response) 'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response
)
self.assertIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
# try practice exam variant # try practice exam variant
rendered_response = get_student_view( rendered_response = get_student_view(
...@@ -659,9 +672,9 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -659,9 +672,9 @@ class ProctoredExamApiTests(LoggedInTestCase):
'is_practice_exam': True, 'is_practice_exam': True,
} }
) )
self.assertIn(self.start_a_practice_exam_msg % self.exam_name, rendered_response) self.assertIn(self.start_a_practice_exam_msg.format(exam_name=self.exam_name), rendered_response)
def test_get_honor_view_with_practice_exam(self): # pylint: disable=invalid-name def test_get_honor_view_with_practice_exam(self):
""" """
Test for get_student_view prompting when the student is enrolled in non-verified Test for get_student_view prompting when the student is enrolled in non-verified
track for a practice exam, this should return not None, meaning track for a practice exam, this should return not None, meaning
...@@ -776,6 +789,26 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -776,6 +789,26 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIsNone(rendered_response) self.assertIsNone(rendered_response)
def test_wrong_exam_combo(self):
"""
Verify that we get a None back when rendering a view
for a practice, non-proctored exam. This is unsupported.
"""
rendered_response = get_student_view(
user_id=self.user_id,
course_id='foo',
content_id='bar',
context={
'is_proctored': False,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
},
user_role='student'
)
self.assertIsNone(rendered_response)
def test_get_disabled_student_view(self): def test_get_disabled_student_view(self):
""" """
Assert that a disabled proctored exam will not override the Assert that a disabled proctored exam will not override the
...@@ -794,7 +827,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -794,7 +827,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
) )
def test_get_studentview_unstarted_exam(self): # pylint: disable=invalid-name def test_get_studentview_unstarted_exam(self):
""" """
Test for get_student_view proctored exam which has not started yet. Test for get_student_view proctored exam which has not started yet.
""" """
...@@ -812,6 +845,28 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -812,6 +845,28 @@ class ProctoredExamApiTests(LoggedInTestCase):
} }
) )
self.assertIn(self.chose_proctored_exam_msg, rendered_response) self.assertIn(self.chose_proctored_exam_msg, rendered_response)
self.assertIn(self.proctored_exam_optout_msg, rendered_response)
def test_get_studentview_unstarted_practice_exam(self):
"""
Test for get_student_view Practice exam which has not started yet.
"""
self._create_unstarted_exam_attempt(is_practice=True)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'is_practice_exam': True,
'default_time_limit_mins': 90
}
)
self.assertIn(self.chose_proctored_exam_msg, rendered_response)
self.assertNotIn(self.proctored_exam_optout_msg, rendered_response)
def test_declined_attempt(self): def test_declined_attempt(self):
""" """
...@@ -854,7 +909,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -854,7 +909,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIn(self.ready_to_start_msg, rendered_response) self.assertIn(self.ready_to_start_msg, rendered_response)
def test_get_studentview_started_exam(self): # pylint: disable=invalid-name def test_get_studentview_started_exam(self):
""" """
Test for get_student_view proctored exam which has started. Test for get_student_view proctored exam which has started.
""" """
...@@ -873,6 +928,44 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -873,6 +928,44 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIsNone(rendered_response) self.assertIsNone(rendered_response)
def test_get_studentview_started_practice_exam(self):
"""
Test for get_student_view practice proctored exam which has started.
"""
self._create_started_practice_exam_attempt()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
def test_get_studentview_started_timed_exam(self):
"""
Test for get_student_view timed exam which has started.
"""
self._create_started_exam_attempt(is_proctored=False)
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_timed,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIsNone(rendered_response)
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True}) @patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_get_studentview_timedout(self): def test_get_studentview_timedout(self):
""" """
...@@ -880,25 +973,23 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -880,25 +973,23 @@ class ProctoredExamApiTests(LoggedInTestCase):
it will automatically state transition into timed_out it will automatically state transition into timed_out
""" """
attempt_obj = self._create_started_exam_attempt() self._create_started_exam_attempt()
reset_time = datetime.now(pytz.UTC) + timedelta(days=1) reset_time = datetime.now(pytz.UTC) + timedelta(days=1)
with freeze_time(reset_time): with freeze_time(reset_time):
get_student_view( with self.assertRaises(NotImplementedError):
user_id=self.user_id, get_student_view(
course_id=self.course_id, user_id=self.user_id,
content_id=self.content_id, course_id=self.course_id,
context={ content_id=self.content_id,
'is_proctored': True, context={
'display_name': self.exam_name, 'is_proctored': True,
'default_time_limit_mins': 90 'display_name': self.exam_name,
} 'default_time_limit_mins': 90
) }
)
attempt = get_exam_attempt_by_id(attempt_obj.id)
self.assertEqual(attempt['status'], 'timed_out') def test_get_studentview_submitted_status(self):
def test_get_studentview_submitted_status(self): # pylint: disable=invalid-name
""" """
Test for get_student_view proctored exam which has been submitted. Test for get_student_view proctored exam which has been submitted.
""" """
...@@ -918,14 +1009,18 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -918,14 +1009,18 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIn(self.proctored_exam_submitted_msg, rendered_response) self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
# test the variant if we are a sample attempt def test_get_studentview_submitted_status_practiceexam(self):
exam_attempt.is_sample_attempt = True """
Test for get_student_view practice exam which has been submitted.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.submitted
exam_attempt.save() exam_attempt.save()
rendered_response = get_student_view( rendered_response = get_student_view(
user_id=self.user_id, user_id=self.user_id,
course_id=self.course_id, course_id=self.course_id,
content_id=self.content_id, content_id=self.content_id_practice,
context={ context={
'is_proctored': True, 'is_proctored': True,
'display_name': self.exam_name, 'display_name': self.exam_name,
...@@ -934,7 +1029,67 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -934,7 +1029,67 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIn(self.practice_exam_submitted_msg, rendered_response) self.assertIn(self.practice_exam_submitted_msg, rendered_response)
def test_get_studentview_rejected_status(self): # pylint: disable=invalid-name def test_get_studentview_created_status_practiceexam(self):
"""
Test for get_student_view practice exam which has been created.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.created
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_created_msg, rendered_response)
def test_get_studentview_ready_to_start_status_practiceexam(self):
"""
Test for get_student_view practice exam which is ready to start.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_start
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.ready_to_start_msg, rendered_response)
def test_get_studentview_compelete_status_practiceexam(self):
"""
Test for get_student_view practice exam when it is complete/ready to submit.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_submit
exam_attempt.save()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.practice_exam_completion_msg, rendered_response)
def test_get_studentview_rejected_status(self):
""" """
Test for get_student_view proctored exam which has been rejected. Test for get_student_view proctored exam which has been rejected.
""" """
...@@ -954,7 +1109,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -954,7 +1109,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIn(self.proctored_exam_rejected_msg, rendered_response) self.assertIn(self.proctored_exam_rejected_msg, rendered_response)
def test_get_studentview_verified_status(self): # pylint: disable=invalid-name def test_get_studentview_verified_status(self):
""" """
Test for get_student_view proctored exam which has been verified. Test for get_student_view proctored exam which has been verified.
""" """
...@@ -974,7 +1129,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -974,7 +1129,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIn(self.proctored_exam_verified_msg, rendered_response) self.assertIn(self.proctored_exam_verified_msg, rendered_response)
def test_get_studentview_completed_status(self): # pylint: disable=invalid-name def test_get_studentview_completed_status(self):
""" """
Test for get_student_view proctored exam which has been completed. Test for get_student_view proctored exam which has been completed.
""" """
...@@ -997,30 +1152,30 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -997,30 +1152,30 @@ class ProctoredExamApiTests(LoggedInTestCase):
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True}) @patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
def test_get_studentview_expired(self): def test_get_studentview_expired(self):
""" """
Test for get_student_view proctored exam which has expired. Test for get_student_view proctored exam which has expired. Since we don't have a template
for that view rendering, it will throw a NotImplementedError
""" """
self._create_started_exam_attempt(started_at=datetime.now(pytz.UTC).replace(year=2010)) self._create_started_exam_attempt(started_at=datetime.now(pytz.UTC).replace(year=2010))
rendered_response = get_student_view( with self.assertRaises(NotImplementedError):
user_id=self.user_id, get_student_view(
course_id=self.course_id, user_id=self.user_id,
content_id=self.content_id, course_id=self.course_id,
context={ content_id=self.content_id,
'is_proctored': True, context={
'display_name': self.exam_name, 'is_proctored': True,
'default_time_limit_mins': 90 'display_name': self.exam_name,
} 'default_time_limit_mins': 90
) }
)
self.assertIn(self.exam_time_expired_msg, rendered_response)
def test_get_studentview_erroneous_exam(self): # pylint: disable=invalid-name def test_get_studentview_erroneous_exam(self):
""" """
Test for get_student_view proctored exam which has exam status error. Test for get_student_view proctored exam which has exam status error.
""" """
exam_attempt = ProctoredExamStudentAttempt.objects.create( ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.proctored_exam_id, proctored_exam_id=self.proctored_exam_id,
user_id=self.user_id, user_id=self.user_id,
external_id=self.external_id, external_id=self.external_id,
...@@ -1041,14 +1196,19 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1041,14 +1196,19 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIn(self.exam_time_error_msg, rendered_response) self.assertIn(self.exam_time_error_msg, rendered_response)
# test the variant if we are a sample attempt def test_get_studentview_erroneous_practice_exam(self):
exam_attempt.is_sample_attempt = True """
Test for get_student_view practice exam which has exam status error.
"""
exam_attempt = self._create_started_practice_exam_attempt()
exam_attempt.status = ProctoredExamStudentAttemptStatus.error
exam_attempt.save() exam_attempt.save()
rendered_response = get_student_view( rendered_response = get_student_view(
user_id=self.user_id, user_id=self.user_id,
course_id=self.course_id, course_id=self.course_id,
content_id=self.content_id, content_id=self.content_id_practice,
context={ context={
'is_proctored': True, 'is_proctored': True,
'display_name': self.exam_name, 'display_name': self.exam_name,
...@@ -1057,7 +1217,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1057,7 +1217,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertIn(self.practice_exam_failed_msg, rendered_response) self.assertIn(self.practice_exam_failed_msg, rendered_response)
def test_get_studentview_unstarted_timed_exam(self): # pylint: disable=invalid-name def test_get_studentview_unstarted_timed_exam(self):
""" """
Test for get_student_view Timed exam which is not proctored and has not started yet. Test for get_student_view Timed exam which is not proctored and has not started yet.
""" """
...@@ -1071,16 +1231,62 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1071,16 +1231,62 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90 'default_time_limit_mins': 90
} }
) )
self.assertNotIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response) self.assertNotIn(
self.assertIn(self.timed_exam_msg % self.exam_name, rendered_response) 'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
self.assertNotIn(self.start_an_exam_msg % self.exam_name, rendered_response) rendered_response
)
self.assertIn(self.timed_exam_msg.format(exam_name=self.exam_name), rendered_response)
self.assertIn('1 hour and 30 minutes', rendered_response)
self.assertNotIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
def test_get_studentview_unstarted_timed_exam_with_allowance(self):
"""
Test for get_student_view Timed exam which is not proctored and has not started yet.
But user has an allowance
"""
ProctoredExamStudentAllowance.objects.create(
proctored_exam_id=self.timed_exam,
user_id=self.user_id,
key='Additional time (minutes)',
value=15
)
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': 90
}
)
self.assertNotIn(
'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response
)
self.assertIn(self.timed_exam_msg.format(exam_name=self.exam_name), rendered_response)
self.assertIn('36 minutes', rendered_response)
self.assertNotIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
def test_get_studentview_completed_timed_exam(self): # pylint: disable=invalid-name @ddt.data(
(
ProctoredExamStudentAttemptStatus.ready_to_submit,
'Are you sure you want to end your timed exam?'
),
(
ProctoredExamStudentAttemptStatus.submitted,
'You have submitted your timed exam'
),
)
@ddt.unpack
def test_get_studentview_completed_timed_exam(self, status, expected_content):
""" """
Test for get_student_view timed exam which has completed. Test for get_student_view timed exam which has completed.
""" """
exam_attempt = self._create_started_exam_attempt(is_proctored=False) exam_attempt = self._create_started_exam_attempt(is_proctored=False)
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_submit exam_attempt.status = status
exam_attempt.save() exam_attempt.save()
rendered_response = get_student_view( rendered_response = get_student_view(
...@@ -1093,7 +1299,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1093,7 +1299,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90 'default_time_limit_mins': 90
} }
) )
self.assertIn(self.timed_exam_completed_msg, rendered_response) self.assertIn(expected_content, rendered_response)
def test_submitted_credit_state(self): def test_submitted_credit_state(self):
""" """
......
...@@ -300,8 +300,6 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -300,8 +300,6 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0))) remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
) )
print attempt['accessibility_time_string']
return Response( return Response(
data=attempt, data=attempt,
status=status.HTTP_200_OK status=status.HTTP_200_OK
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment