Commit dd9d8b51 by chrisndodge

Merge pull request #50 from edx/afzaledx/phx-20-allow-practice-exam

PHX-20 Practice Exam
parents 29c4c4b5 44a1dac6
...@@ -45,7 +45,7 @@ def is_feature_enabled(): ...@@ -45,7 +45,7 @@ def is_feature_enabled():
def create_exam(course_id, content_id, exam_name, time_limit_mins, def create_exam(course_id, content_id, exam_name, time_limit_mins,
is_proctored=True, external_id=None, is_active=True): is_proctored=True, is_practice_exam=False, external_id=None, is_active=True):
""" """
Creates a new ProctoredExam entity, if the course_id/content_id pair do not already exist. Creates a new ProctoredExam entity, if the course_id/content_id pair do not already exist.
If that pair already exists, then raise exception. If that pair already exists, then raise exception.
...@@ -62,13 +62,14 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins, ...@@ -62,13 +62,14 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins,
exam_name=exam_name, exam_name=exam_name,
time_limit_mins=time_limit_mins, time_limit_mins=time_limit_mins,
is_proctored=is_proctored, is_proctored=is_proctored,
is_practice_exam=is_practice_exam,
is_active=is_active is_active=is_active
) )
return proctored_exam.id return proctored_exam.id
def update_exam(exam_id, exam_name=None, time_limit_mins=None, def update_exam(exam_id, exam_name=None, time_limit_mins=None,
is_proctored=None, external_id=None, is_active=None): is_proctored=None, is_practice_exam=None, external_id=None, is_active=None):
""" """
Given a Django ORM id, update the existing record, otherwise raise exception if not found. Given a Django ORM id, update the existing record, otherwise raise exception if not found.
If an argument is not passed in, then do not change it's current value. If an argument is not passed in, then do not change it's current value.
...@@ -85,6 +86,8 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None, ...@@ -85,6 +86,8 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None,
proctored_exam.time_limit_mins = time_limit_mins proctored_exam.time_limit_mins = time_limit_mins
if is_proctored is not None: if is_proctored is not None:
proctored_exam.is_proctored = is_proctored proctored_exam.is_proctored = is_proctored
if is_practice_exam is not None:
proctored_exam.is_practice_exam = is_practice_exam
if external_id is not None: if external_id is not None:
proctored_exam.external_id = external_id proctored_exam.external_id = external_id
if is_active is not None: if is_active is not None:
...@@ -210,16 +213,22 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -210,16 +213,22 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
one exam_attempt per user per exam. Multiple attempts by user will be archived one exam_attempt per user per exam. Multiple attempts by user will be archived
in a separate table in a separate table
""" """
if ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id):
err_msg = (
'Cannot create new exam attempt for exam_id = {exam_id} and '
'user_id = {user_id} because it already exists!'
).format(exam_id=exam_id, user_id=user_id)
raise StudentExamAttemptAlreadyExistsException(err_msg)
# for now the student is allowed the exam default # for now the student is allowed the exam default
exam = get_exam_by_id(exam_id) exam = get_exam_by_id(exam_id)
existing_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
if existing_attempt:
if existing_attempt.is_sample_attempt:
# Archive the existing attempt by deleting it.
existing_attempt.delete_exam_attempt()
else:
err_msg = (
'Cannot create new exam attempt for exam_id = {exam_id} and '
'user_id = {user_id} because it already exists!'
).format(exam_id=exam_id, user_id=user_id)
raise StudentExamAttemptAlreadyExistsException(err_msg)
allowed_time_limit_mins = exam['time_limit_mins'] allowed_time_limit_mins = exam['time_limit_mins']
# add in the allowed additional time # add in the allowed additional time
...@@ -259,7 +268,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -259,7 +268,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
context={ context={
'time_limit_mins': allowed_time_limit_mins, 'time_limit_mins': allowed_time_limit_mins,
'attempt_code': attempt_code, 'attempt_code': attempt_code,
'is_sample_attempt': False, 'is_sample_attempt': exam['is_practice_exam'],
'callback_url': callback_url, 'callback_url': callback_url,
'full_name': full_name, 'full_name': full_name,
} }
...@@ -272,7 +281,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -272,7 +281,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
allowed_time_limit_mins, allowed_time_limit_mins,
attempt_code, attempt_code,
taking_as_proctored, taking_as_proctored,
False, exam['is_practice_exam'],
external_id external_id
) )
return attempt.id return attempt.id
...@@ -496,17 +505,6 @@ def get_student_view(user_id, course_id, content_id, ...@@ -496,17 +505,6 @@ def get_student_view(user_id, course_id, content_id,
if user_role != 'student': if user_role != 'student':
return None return None
# see if only 'verified' track students should see this
check_mode = (
settings.PROCTORING_SETTINGS.get('MUST_BE_VERIFIED_TRACK', True) and
'credit_state' in context and
context['credit_state']
)
if check_mode:
if context['credit_state']['enrollment_mode'] != 'verified':
return None
student_view_template = None student_view_template = None
exam_id = None exam_id = None
...@@ -519,18 +517,37 @@ def get_student_view(user_id, course_id, content_id, ...@@ -519,18 +517,37 @@ def get_student_view(user_id, course_id, content_id,
return None return None
exam_id = exam['id'] exam_id = exam['id']
is_proctored = exam['is_proctored']
except ProctoredExamNotFoundException: except ProctoredExamNotFoundException:
# This really shouldn't happen # This really shouldn't happen
# as Studio will be setting this up # as Studio will be setting this up
is_proctored = context.get('is_proctored', False)
exam_id = create_exam( exam_id = create_exam(
course_id=course_id, course_id=course_id,
content_id=unicode(content_id), content_id=unicode(content_id),
exam_name=context['display_name'], exam_name=context['display_name'],
time_limit_mins=context['default_time_limit_mins'], time_limit_mins=context['default_time_limit_mins'],
is_proctored=is_proctored 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_proctored = exam['is_proctored']
# see if only 'verified' track students should see this *except* if it is a practice exam
check_mode = (
settings.PROCTORING_SETTINGS.get('MUST_BE_VERIFIED_TRACK', True) and
'credit_state' in context and
context['credit_state'] and not
exam['is_practice_exam']
)
if check_mode:
# Allow only the verified students to take the exam as a proctored exam
# Also make an exception for the honor students to take the "practice exam" as a proctored exam.
# For the rest of the enrollment modes, None is returned which shows the exam content
# to the student rather than the proctoring prompt.
if context['credit_state']['enrollment_mode'] != 'verified':
return None
attempt = get_exam_attempt(exam_id, user_id) attempt = get_exam_attempt(exam_id, user_id)
has_started_exam = attempt and attempt.get('started_at') has_started_exam = attempt and attempt.get('started_at')
...@@ -555,7 +572,10 @@ def get_student_view(user_id, course_id, content_id, ...@@ -555,7 +572,10 @@ def get_student_view(user_id, course_id, content_id,
if is_proctored: if is_proctored:
if not attempt: if not attempt:
student_view_template = 'proctoring/seq_proctored_exam_entrance.html' 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: else:
provider = get_backend_provider() provider = get_backend_provider()
student_view_template = 'proctoring/seq_proctored_exam_instructions.html' student_view_template = 'proctoring/seq_proctored_exam_instructions.html'
...@@ -568,7 +588,10 @@ def get_student_view(user_id, course_id, content_id, ...@@ -568,7 +588,10 @@ def get_student_view(user_id, course_id, content_id,
elif attempt['status'] == ProctoredExamStudentAttemptStatus.timed_out: elif attempt['status'] == ProctoredExamStudentAttemptStatus.timed_out:
student_view_template = 'proctoring/seq_timed_exam_expired.html' student_view_template = 'proctoring/seq_timed_exam_expired.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.submitted: elif attempt['status'] == ProctoredExamStudentAttemptStatus.submitted:
student_view_template = 'proctoring/seq_proctored_exam_submitted.html' 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: elif attempt['status'] == ProctoredExamStudentAttemptStatus.verified:
student_view_template = 'proctoring/seq_proctored_exam_verified.html' student_view_template = 'proctoring/seq_proctored_exam_verified.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.rejected: elif attempt['status'] == ProctoredExamStudentAttemptStatus.rejected:
...@@ -597,6 +620,7 @@ def get_student_view(user_id, course_id, content_id, ...@@ -597,6 +620,7 @@ def get_student_view(user_id, course_id, content_id,
'total_time': total_time, 'total_time': total_time,
'exam_id': exam_id, 'exam_id': exam_id,
'progress_page_url': progress_page_url, 'progress_page_url': progress_page_url,
'is_sample_attempt': attempt['is_sample_attempt'] if attempt else False,
'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'), 'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
'exam_started_poll_url': reverse( 'exam_started_poll_url': reverse(
'edx_proctoring.proctored_exam.attempt', 'edx_proctoring.proctored_exam.attempt',
......
...@@ -37,6 +37,9 @@ class ProctoredExam(TimeStampedModel): ...@@ -37,6 +37,9 @@ class ProctoredExam(TimeStampedModel):
# Whether this exam actually is proctored or not. # Whether this exam actually is proctored or not.
is_proctored = models.BooleanField() is_proctored = models.BooleanField()
# Whether this exam is for practice only.
is_practice_exam = models.BooleanField()
# Whether this exam will be active. # Whether this exam will be active.
is_active = models.BooleanField() is_active = models.BooleanField()
...@@ -118,7 +121,7 @@ class ProctoredExamStudentAttemptManager(models.Manager): ...@@ -118,7 +121,7 @@ class ProctoredExamStudentAttemptManager(models.Manager):
Returns the Student Exam Attempts for the given course_id. Returns the Student Exam Attempts for the given course_id.
""" """
return self.filter(proctored_exam__course_id=course_id) return self.filter(proctored_exam__course_id=course_id).order_by('-created')
def get_filtered_exam_attempts(self, course_id, search_by): def get_filtered_exam_attempts(self, course_id, search_by):
""" """
...@@ -128,7 +131,7 @@ class ProctoredExamStudentAttemptManager(models.Manager): ...@@ -128,7 +131,7 @@ class ProctoredExamStudentAttemptManager(models.Manager):
Q(user__username__contains=search_by) | Q(user__email__contains=search_by) Q(user__username__contains=search_by) | Q(user__email__contains=search_by)
) )
return self.filter(filtered_query) return self.filter(filtered_query).order_by('-created')
def get_active_student_attempts(self, user_id, course_id=None): def get_active_student_attempts(self, user_id, course_id=None):
""" """
...@@ -219,7 +222,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -219,7 +222,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# in case there is an option to opt-out # in case there is an option to opt-out
taking_as_proctored = models.BooleanField() taking_as_proctored = models.BooleanField()
# Whether this attampt is considered a sample attempt, e.g. to try out # Whether this attempt is considered a sample attempt, e.g. to try out
# the proctoring software # the proctoring software
is_sample_attempt = models.BooleanField() is_sample_attempt = models.BooleanField()
...@@ -266,7 +269,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -266,7 +269,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
def delete_exam_attempt(self): def delete_exam_attempt(self):
""" """
deletes the exam attempt object. deletes the exam attempt object and archives it to the ProctoredExamStudentAttemptHistory table.
""" """
self.delete() self.delete()
......
...@@ -29,6 +29,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer): ...@@ -29,6 +29,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
time_limit_mins = serializers.IntegerField(required=True) time_limit_mins = serializers.IntegerField(required=True)
is_active = StrictBooleanField(required=True) is_active = StrictBooleanField(required=True)
is_practice_exam = StrictBooleanField(required=True)
is_proctored = StrictBooleanField(required=True) is_proctored = StrictBooleanField(required=True)
class Meta: class Meta:
...@@ -39,7 +40,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer): ...@@ -39,7 +40,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
fields = ( fields = (
"id", "course_id", "content_id", "external_id", "exam_name", "id", "course_id", "content_id", "external_id", "exam_name",
"time_limit_mins", "is_proctored", "is_active" "time_limit_mins", "is_proctored", "is_practice_exam", "is_active"
) )
......
{% load i18n %}
<div class="sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
Would you like to take {{ display_name }} as a practice proctored exam?
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
Since this course is offered for credit, there will be one or more proctored exams in this course. Before you encounter a proctored exam that will count towards your grade and credit eligibility. You now have the option to take a practice proctored exam. Online proctoring is one requirement towards being eligible for credit.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
This practice proctored exam is being provided to you so that you can learn more about proctoring. Also this
practice exam will give you a chance to test your computer system with the proctoring software without it
counting towards your grade.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
While it is not requireed to take this practice proctored exam, it is highly recommended.
{% endblocktrans %}
</p>
<div class="gated-sequence">
<span><i class="fa fa-lock"></i></span>
<a class="start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false>
{% trans "Yes, I want to take this practice exam with online proctoring" %}
</a>
<p>
{% blocktrans %}
You will be guided through installing {{platform_name}} approved online proctoring software and
performing various checks to set up your proctored exam session. Have your photo ID
ready for the photo ID verification step.<br />
Immediately after you complete the set up, you will begin your timed and proctored exam.<br />
Before you set up your proctoring session, you might want to <a href="#">read this</a>
to learn more about taking a proctored exam.<br />
{% endblocktrans %}
</p>
<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>
</div>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
<script type="text/javascript">
$('.start-timed-exam').click(
function(event) {
var action_url = $(this).data('ajax-url');
var exam_id = $(this).data('exam-id');
var attempt_proctored = $(this).data('attempt-proctored');
var start_immediately = $(this).data('start-immediately');
if (typeof action_url === "undefined" ) {
return false;
}
$.post(
action_url,
{
"exam_id": exam_id,
"attempt_proctored": attempt_proctored,
"start_clock": start_immediately
},
function(data) {
// reload the page, because we've unlocked it
location.reload();
}
);
}
);
</script>
{% load i18n %}
<div class="sequence proctored-exam completed" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
You have submitted this practice proctored exam
{% endblocktrans %}
</h3>
<h4>
{% blocktrans %}
Your Practice Proctoring Session: <b> Completed </b>
{% endblocktrans %}
</h4>
<p>
{% blocktrans %}
As practice exams do not count towards a grade or credit eligibility
they are not reviewed. You have completed your practice exam and should continue
with the rest of your course
{% endblocktrans %}
</p>
<div class="gated-sequence">
<span><i class="fa fa-lock"></i></span>
<a class="start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false>
{% trans "You can also retry this practice exam" %}
</a>
</div>
</div>
<script type="text/javascript">
$('.start-timed-exam').click(
function(event) {
var action_url = $(this).data('ajax-url');
var exam_id = $(this).data('exam-id');
var attempt_proctored = $(this).data('attempt-proctored');
var start_immediately = $(this).data('start-immediately');
if (typeof action_url === "undefined" ) {
return false;
}
$.post(
action_url,
{
"exam_id": exam_id,
"attempt_proctored": attempt_proctored,
"start_clock": start_immediately
},
function(data) {
// reload the page, because we've unlocked it
location.reload();
}
);
}
);
</script>
...@@ -64,6 +64,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -64,6 +64,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.course_id = 'test_course' self.course_id = 'test_course'
self.content_id = 'test_content_id' self.content_id = 'test_content_id'
self.content_id_timed = 'test_content_id_timed' self.content_id_timed = 'test_content_id_timed'
self.content_id_practice = 'test_content_id_practice'
self.disabled_content_id = 'test_disabled_content_id' self.disabled_content_id = 'test_disabled_content_id'
self.exam_name = 'Test Exam' self.exam_name = 'Test Exam'
self.user_id = self.user.id self.user_id = self.user.id
...@@ -72,6 +73,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -72,6 +73,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.external_id = 'test_external_id' self.external_id = 'test_external_id'
self.proctored_exam_id = self._create_proctored_exam() self.proctored_exam_id = self._create_proctored_exam()
self.timed_exam = self._create_timed_exam() self.timed_exam = self._create_timed_exam()
self.practice_exam_id = self._create_practice_exam()
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
...@@ -85,6 +87,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -85,6 +87,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
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.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.practice_exam_submitted_msg = 'You have submitted this practice proctored exam'
def _create_proctored_exam(self): def _create_proctored_exam(self):
""" """
...@@ -109,6 +113,19 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -109,6 +113,19 @@ class ProctoredExamApiTests(LoggedInTestCase):
is_proctored=False is_proctored=False
) )
def _create_practice_exam(self):
"""
Calls the api's create_exam to create a practice exam object.
"""
return create_exam(
course_id=self.course_id,
content_id=self.content_id_practice,
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_practice_exam=True,
is_proctored=False
)
def _create_disabled_exam(self): def _create_disabled_exam(self):
""" """
Calls the api's create_exam to create an exam object. Calls the api's create_exam to create an exam object.
...@@ -145,6 +162,20 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -145,6 +162,20 @@ class ProctoredExamApiTests(LoggedInTestCase):
allowed_time_limit_mins=10 allowed_time_limit_mins=10
) )
def _create_started_practice_exam_attempt(self, started_at=None): # pylint: disable=invalid-name
"""
Creates the ProctoredExamStudentAttempt object.
"""
return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.practice_exam_id,
user_id=self.user_id,
external_id=self.external_id,
started_at=started_at if started_at else datetime.now(pytz.UTC),
is_sample_attempt=True,
status=ProctoredExamStudentAttemptStatus.started,
allowed_time_limit_mins=10
)
def _add_allowance_for_user(self): def _add_allowance_for_user(self):
""" """
creates allowance for user. creates allowance for user.
...@@ -173,6 +204,24 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -173,6 +204,24 @@ class ProctoredExamApiTests(LoggedInTestCase):
with self.assertRaises(ProctoredExamAlreadyExists): with self.assertRaises(ProctoredExamAlreadyExists):
self._create_proctored_exam() self._create_proctored_exam()
def test_update_practice_exam(self):
"""
test update the existing practice exam to increase the time limit.
"""
updated_practice_exam_id = update_exam(
self.practice_exam_id, time_limit_mins=31, is_practice_exam=True
)
# only those fields were updated, whose
# values are passed.
self.assertEqual(self.practice_exam_id, updated_practice_exam_id)
update_practice_exam = ProctoredExam.objects.get(id=updated_practice_exam_id)
self.assertEqual(update_practice_exam.time_limit_mins, 31)
self.assertEqual(update_practice_exam.course_id, 'test_course')
self.assertEqual(update_practice_exam.content_id, 'test_content_id_practice')
def test_update_proctored_exam(self): def test_update_proctored_exam(self):
""" """
test update the existing proctored exam test update the existing proctored exam
...@@ -217,7 +266,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -217,7 +266,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertEqual(proctored_exam['exam_name'], self.exam_name) self.assertEqual(proctored_exam['exam_name'], self.exam_name)
exams = get_all_exams_for_course(self.course_id) exams = get_all_exams_for_course(self.course_id)
self.assertEqual(len(exams), 3) self.assertEqual(len(exams), 4)
def test_get_invalid_proctored_exam(self): def test_get_invalid_proctored_exam(self):
""" """
...@@ -320,7 +369,15 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -320,7 +369,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
proctored_exam_student_attempt = self._create_unstarted_exam_attempt() proctored_exam_student_attempt = self._create_unstarted_exam_attempt()
with self.assertRaises(StudentExamAttemptAlreadyExistsException): with self.assertRaises(StudentExamAttemptAlreadyExistsException):
create_exam_attempt(proctored_exam_student_attempt.proctored_exam, 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
"""
Taking the practice exam several times should not cause an exception.
"""
practice_exam_student_attempt = self._create_started_practice_exam_attempt()
new_attempt_id = create_exam_attempt(practice_exam_student_attempt.proctored_exam.id, self.user_id)
self.assertGreater(practice_exam_student_attempt, new_attempt_id, "New attempt not created.")
def test_get_exam_attempt(self): def test_get_exam_attempt(self):
""" """
...@@ -473,8 +530,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -473,8 +530,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
filtered_attempts = get_filtered_exam_attempts(self.course_id, self.user.username) filtered_attempts = get_filtered_exam_attempts(self.course_id, self.user.username)
self.assertEqual(len(filtered_attempts), 2) self.assertEqual(len(filtered_attempts), 2)
self.assertEqual(filtered_attempts[0]['id'], exam_attempt.id) self.assertEqual(filtered_attempts[0]['id'], new_exam_attempt)
self.assertEqual(filtered_attempts[1]['id'], new_exam_attempt) self.assertEqual(filtered_attempts[1]['id'], exam_attempt.id)
def test_get_all_exam_attempts(self): def test_get_all_exam_attempts(self):
""" """
...@@ -493,8 +550,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -493,8 +550,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
all_exams = get_all_exam_attempts(self.course_id) all_exams = get_all_exam_attempts(self.course_id)
self.assertEqual(len(all_exams), 2) self.assertEqual(len(all_exams), 2)
self.assertEqual(all_exams[0]['id'], exam_attempt.id) self.assertEqual(all_exams[0]['id'], updated_exam_attempt_id)
self.assertEqual(all_exams[1]['id'], updated_exam_attempt_id) self.assertEqual(all_exams[1]['id'], exam_attempt.id)
def test_get_student_view(self): def test_get_student_view(self):
""" """
...@@ -514,9 +571,45 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -514,9 +571,45 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response) 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(self.start_an_exam_msg % self.exam_name, rendered_response)
def test_get_honot_view(self): # try practice exam variant
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id + 'foo',
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'is_practice_exam': True,
}
)
self.assertIn(self.start_a_practice_exam_msg % self.exam_name, rendered_response)
def test_get_honor_view_with_practice_exam(self): # pylint: disable=invalid-name
""" """
Test for get_student_view promting 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
student will see proctored content
"""
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,
'credit_state': {
'enrollment_mode': 'honor'
},
'is_practice_exam': True
}
)
self.assertIsNotNone(rendered_response)
def test_get_honor_view(self):
"""
Test for get_student_view prompting when the student is enrolled in non-verified
track, this should return None track, this should return None
""" """
rendered_response = get_student_view( rendered_response = get_student_view(
...@@ -529,7 +622,8 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -529,7 +622,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90, 'default_time_limit_mins': 90,
'credit_state': { 'credit_state': {
'enrollment_mode': 'honor' 'enrollment_mode': 'honor'
} },
'is_practice_exam': False
} }
) )
self.assertIsNone(rendered_response) self.assertIsNone(rendered_response)
...@@ -629,6 +723,22 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -629,6 +723,22 @@ 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
exam_attempt.is_sample_attempt = True
exam_attempt.save()
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.practice_exam_submitted_msg, rendered_response)
def test_get_studentview_rejected_status(self): # pylint: disable=invalid-name def test_get_studentview_rejected_status(self): # pylint: disable=invalid-name
""" """
Test for get_student_view proctored exam which has been rejected. Test for get_student_view proctored exam which has been rejected.
......
...@@ -22,11 +22,15 @@ class TestProctoredExamSerializer(unittest.TestCase): ...@@ -22,11 +22,15 @@ class TestProctoredExamSerializer(unittest.TestCase):
'time_limit_mins': 90, 'time_limit_mins': 90,
'external_id': '123', 'external_id': '123',
'is_proctored': 'bla', 'is_proctored': 'bla',
'is_practice_exam': 'bla',
'is_active': 'f' 'is_active': 'f'
} }
serializer = ProctoredExamSerializer(data=data) serializer = ProctoredExamSerializer(data=data)
self.assertFalse(serializer.is_valid()) self.assertFalse(serializer.is_valid())
self.assertDictEqual( self.assertDictEqual(
{'is_proctored': [u'This field is required.']}, serializer.errors {
'is_proctored': [u'This field is required.'],
'is_practice_exam': [u'This field is required.'],
}, serializer.errors
) )
...@@ -85,6 +85,7 @@ class ProctoredExamViewTests(LoggedInTestCase): ...@@ -85,6 +85,7 @@ class ProctoredExamViewTests(LoggedInTestCase):
'time_limit_mins': 90, 'time_limit_mins': 90,
'external_id': '123', 'external_id': '123',
'is_proctored': True, 'is_proctored': True,
'is_practice_exam': False,
'is_active': True 'is_active': True
} }
response = self.client.post( response = self.client.post(
...@@ -119,6 +120,7 @@ class ProctoredExamViewTests(LoggedInTestCase): ...@@ -119,6 +120,7 @@ class ProctoredExamViewTests(LoggedInTestCase):
'time_limit_mins': 90, 'time_limit_mins': 90,
'external_id': '123', 'external_id': '123',
'is_proctored': True, 'is_proctored': True,
'is_practice_exam': False,
'is_active': True 'is_active': True
} }
response = self.client.post( response = self.client.post(
......
...@@ -81,6 +81,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -81,6 +81,7 @@ class ProctoredExamView(AuthenticatedAPIView):
"exam_name": "Midterm", "exam_name": "Midterm",
"time_limit_mins": 90, "time_limit_mins": 90,
"is_proctored": true, "is_proctored": true,
"is_practice_exam": false,
"external_id": "12213DASAD", "external_id": "12213DASAD",
"is_active": true "is_active": true
} }
...@@ -91,6 +92,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -91,6 +92,7 @@ class ProctoredExamView(AuthenticatedAPIView):
* exam_name: This is the display name of the Exam (Midterm etc). * exam_name: This is the display name of the Exam (Midterm etc).
* time_limit_mins: Time limit (in minutes) that a student can finish this exam. * time_limit_mins: Time limit (in minutes) that a student can finish this exam.
* is_proctored: Whether this exam actually is proctored or not. * is_proctored: Whether this exam actually is proctored or not.
* is_proctored: Whether this exam will be for practice only.
* external_id: This will be a integration specific ID - say to SoftwareSecure. * external_id: This will be a integration specific ID - say to SoftwareSecure.
* is_active: Whether this exam will be active. * is_active: Whether this exam will be active.
...@@ -108,6 +110,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -108,6 +110,7 @@ class ProctoredExamView(AuthenticatedAPIView):
"exam_name": "Final", "exam_name": "Final",
"time_limit_mins": 120, "time_limit_mins": 120,
"is_proctored": true, "is_proctored": true,
"is_practice_exam": false,
"external_id": 235 "external_id": 235
"is_active": true "is_active": true
} }
...@@ -140,6 +143,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -140,6 +143,7 @@ class ProctoredExamView(AuthenticatedAPIView):
exam_name=request.DATA.get('exam_name', None), exam_name=request.DATA.get('exam_name', None),
time_limit_mins=request.DATA.get('time_limit_mins', None), time_limit_mins=request.DATA.get('time_limit_mins', None),
is_proctored=request.DATA.get('is_proctored', None), is_proctored=request.DATA.get('is_proctored', None),
is_practice_exam=request.DATA.get('is_practice_exam', None),
external_id=request.DATA.get('external_id', None), external_id=request.DATA.get('external_id', None),
is_active=request.DATA.get('is_active', None) is_active=request.DATA.get('is_active', None)
) )
...@@ -162,6 +166,7 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -162,6 +166,7 @@ class ProctoredExamView(AuthenticatedAPIView):
exam_name=request.DATA.get('exam_name', None), exam_name=request.DATA.get('exam_name', None),
time_limit_mins=request.DATA.get('time_limit_mins', None), time_limit_mins=request.DATA.get('time_limit_mins', None),
is_proctored=request.DATA.get('is_proctored', None), is_proctored=request.DATA.get('is_proctored', None),
is_practice_exam=request.DATA.get('is_practice_exam', None),
external_id=request.DATA.get('external_id', None), external_id=request.DATA.get('external_id', None),
is_active=request.DATA.get('is_active', None), is_active=request.DATA.get('is_active', None),
) )
......
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