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):
return summary
def get_student_view(user_id, course_id, content_id,
context, user_role='student'):
def _does_time_remain(attempt):
"""
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
Helper function returns True if time remains for an attempt and False
otherwise. Called from _get_timed_exam_view(), _get_practice_exam_view()
and _get_proctored_exam_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
attempt = get_exam_attempt(exam_id, user_id)
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
attempt_status = attempt['status'] if attempt else 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)
if not attempt_status:
student_view_template = 'timed_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.ready_to_submit:
student_view_template = 'timed_exam/ready_to_submit.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
student_view_template = 'timed_exam/submitted.html'
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
check_mode_and_eligibility = (
......@@ -1095,98 +1220,95 @@ def get_student_view(user_id, course_id, content_id,
# don't override context, let the courseware show
return None
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
# proctored exam
if attempt and attempt['status'] == ProctoredExamStudentAttemptStatus.declined:
if attempt_status == ProctoredExamStudentAttemptStatus.declined:
return None
does_time_remain = False
has_started_exam = attempt and attempt.get('started_at')
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
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:
if not attempt_status:
student_view_template = 'proctored_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 = 'proctoring/seq_proctored_exam_instructions.html'
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 = 'proctoring/seq_proctored_exam_ready_to_start.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.error:
if attempt['is_sample_attempt']:
student_view_template = 'proctoring/seq_proctored_practice_exam_error.html'
else:
student_view_template = 'proctoring/seq_proctored_exam_error.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.timed_out:
student_view_template = 'proctoring/seq_timed_exam_expired.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.submitted:
if attempt['is_sample_attempt']:
student_view_template = 'proctoring/seq_proctored_practice_exam_submitted.html'
else:
student_view_template = 'proctoring/seq_proctored_exam_submitted.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'
elif attempt_status == ProctoredExamStudentAttemptStatus.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:
raise NotImplementedError('There is no defined rendering for ProctoredExamStudentAttemptStatus.timed_out!')
elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
student_view_template = 'proctored_exam/submitted.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.verified:
student_view_template = 'proctored_exam/verified.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.rejected:
student_view_template = 'proctored_exam/rejected.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)
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]
)
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', {}),
})
django_context.update(_get_proctored_exam_context(exam, attempt, course_id))
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
......@@ -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'])
template = loader.get_template('proctoring/proctoring_launch_callback.html')
template = loader.get_template('proctored_exam/proctoring_launch_callback.html')
poll_url = reverse(
'edx_proctoring.anonymous.proctoring_poll_status',
......
......@@ -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>
</button>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctored_exam/footer.html' %}
<script type="text/javascript">
......
......@@ -26,7 +26,7 @@
</a>
</button>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctored_exam/footer.html' %}
<script type="text/javascript">
$('.start-timed-exam').click(
function(event) {
......
"""
We need python think this is a python module
"""
......@@ -64,7 +64,7 @@
<div class="clearfix"></div>
</div>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctored_exam/footer.html' %}
<script type="text/javascript">
var startProctoredExam = function(selector, exam_id, action_url, start_immediately, attempt_proctored) {
......@@ -90,7 +90,7 @@
alert(msg);
});
};
var inProcess = false;
var disableClickEvent = function (selector) {
......@@ -110,7 +110,7 @@
$( this ).css('cursor', 'pointer');
});
};
$('.start-timed-exam').click(
function() {
if (!inProcess) {
......
......@@ -33,4 +33,4 @@
</p>
<div class="clearfix"></div>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctored_exam/footer.html' %}
......@@ -55,7 +55,7 @@
</p>
{% endif %}
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctored_exam/footer.html' %}
<script type="text/javascript">
......
......@@ -33,7 +33,7 @@
</div>
</div>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctored_exam/footer.html' %}
<script type="text/javascript">
......
......@@ -33,4 +33,4 @@
</p>
<div class="clearfix"></div>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctored_exam/footer.html' %}
......@@ -30,4 +30,4 @@
{% endblocktrans %}
</p>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctored_exam/footer.html' %}
......@@ -26,4 +26,4 @@
{% endblocktrans %}
</p>
</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 @@
</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." %}
</p>
<div class="gated-sequence">
<a class='start-timed-exam' data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}">
<button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}">
<a>
{% blocktrans %}
I'm ready! Start this timed exam.
{% endblocktrans %}
<i class="fa fa-arrow-circle-right"></i>
</a>
</div>
<i class="fa fa-arrow-circle-right"></i>
</a>
</button>
</div>
{% include 'proctoring/seq_timed_exam_footer.html' %}
{% include 'timed_exam/footer.html' %}
<script type="text/javascript">
......
......@@ -2,7 +2,7 @@
<div class="sequence timed-exam completed" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
This is the end of your timed exam
Are you sure you want to end your timed exam?
{% endblocktrans %}
</h3>
<p>
......@@ -11,11 +11,22 @@
and your worked will then be graded.
{% endblocktrans %}
</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}}" >
{% blocktrans %}
I'm ready! Submit my answers and end my timed exam
{% endblocktrans %}
</button>
<div>
<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}}">
{% blocktrans %}
Yes, end my timed exam
{% 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>
<script type="text/javascript">
$('.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):
self.disabled_exam_id = self._create_disabled_exam()
# Messages for get_student_view
self.start_an_exam_msg = 'Would you like to take "%s" as a proctored exam?'
self.timed_exam_msg = '%s is a Timed Exam'
self.start_an_exam_msg = 'Would you like to take "{exam_name}" as a proctored 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_error_msg = 'There was a problem with your proctoring session'
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_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_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 "%s" as a practice proctored exam?'
self.start_a_practice_exam_msg = 'Would you like to take "{exam_name}" as a 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.practice_exam_failed_msg = 'There was a problem with your practice proctoring session'
self.proctored_exam_email_subject = 'Proctoring Session Results Update'
......@@ -163,12 +165,20 @@ class ProctoredExamApiTests(LoggedInTestCase):
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.
"""
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(
proctored_exam_id=self.proctored_exam_id if is_proctored else self.timed_exam,
proctored_exam_id=exam_id,
user_id=self.user_id,
external_id=self.external_id,
allowed_time_limit_mins=10,
......@@ -190,7 +200,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
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.
"""
......@@ -411,7 +421,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
with self.assertRaises(StudentExamAttemptAlreadyExistsException):
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.
"""
......@@ -644,8 +654,11 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90
}
)
self.assertIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response)
self.assertIn(self.start_an_exam_msg % self.exam_name, rendered_response)
self.assertIn(
'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
rendered_response = get_student_view(
......@@ -659,9 +672,9 @@ class ProctoredExamApiTests(LoggedInTestCase):
'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
track for a practice exam, this should return not None, meaning
......@@ -776,6 +789,26 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
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):
"""
Assert that a disabled proctored exam will not override the
......@@ -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.
"""
......@@ -812,6 +845,28 @@ class ProctoredExamApiTests(LoggedInTestCase):
}
)
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):
"""
......@@ -854,7 +909,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
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.
"""
......@@ -873,6 +928,44 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
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})
def test_get_studentview_timedout(self):
"""
......@@ -880,25 +973,23 @@ class ProctoredExamApiTests(LoggedInTestCase):
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)
with freeze_time(reset_time):
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'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): # pylint: disable=invalid-name
with self.assertRaises(NotImplementedError):
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
def test_get_studentview_submitted_status(self):
"""
Test for get_student_view proctored exam which has been submitted.
"""
......@@ -918,14 +1009,18 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
self.assertIn(self.proctored_exam_submitted_msg, rendered_response)
# test the variant if we are a sample attempt
exam_attempt.is_sample_attempt = True
def test_get_studentview_submitted_status_practiceexam(self):
"""
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()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
......@@ -934,7 +1029,67 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
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.
"""
......@@ -954,7 +1109,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
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.
"""
......@@ -974,7 +1129,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
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.
"""
......@@ -997,30 +1152,30 @@ class ProctoredExamApiTests(LoggedInTestCase):
@patch.dict('django.conf.settings.PROCTORING_SETTINGS', {'ALLOW_TIMED_OUT_STATE': True})
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))
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.exam_time_expired_msg, rendered_response)
with self.assertRaises(NotImplementedError):
get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
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.
"""
exam_attempt = ProctoredExamStudentAttempt.objects.create(
ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.proctored_exam_id,
user_id=self.user_id,
external_id=self.external_id,
......@@ -1041,14 +1196,19 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
self.assertIn(self.exam_time_error_msg, rendered_response)
# test the variant if we are a sample attempt
exam_attempt.is_sample_attempt = True
def test_get_studentview_erroneous_practice_exam(self):
"""
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()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
content_id=self.content_id_practice,
context={
'is_proctored': True,
'display_name': self.exam_name,
......@@ -1057,7 +1217,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
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.
"""
......@@ -1071,16 +1231,62 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90
}
)
self.assertNotIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response)
self.assertIn(self.timed_exam_msg % self.exam_name, rendered_response)
self.assertNotIn(self.start_an_exam_msg % self.exam_name, rendered_response)
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('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.
"""
exam_attempt = self._create_started_exam_attempt(is_proctored=False)
exam_attempt.status = ProctoredExamStudentAttemptStatus.ready_to_submit
exam_attempt.status = status
exam_attempt.save()
rendered_response = get_student_view(
......@@ -1093,7 +1299,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
'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):
"""
......
......@@ -300,8 +300,6 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
)
print attempt['accessibility_time_string']
return Response(
data=attempt,
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