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():
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.
If that pair already exists, then raise exception.
......@@ -62,13 +62,14 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins,
exam_name=exam_name,
time_limit_mins=time_limit_mins,
is_proctored=is_proctored,
is_practice_exam=is_practice_exam,
is_active=is_active
)
return proctored_exam.id
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.
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,
proctored_exam.time_limit_mins = time_limit_mins
if is_proctored is not None:
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:
proctored_exam.external_id = external_id
if is_active is not None:
......@@ -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
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
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']
# add in the allowed additional time
......@@ -259,7 +268,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
context={
'time_limit_mins': allowed_time_limit_mins,
'attempt_code': attempt_code,
'is_sample_attempt': False,
'is_sample_attempt': exam['is_practice_exam'],
'callback_url': callback_url,
'full_name': full_name,
}
......@@ -272,7 +281,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
allowed_time_limit_mins,
attempt_code,
taking_as_proctored,
False,
exam['is_practice_exam'],
external_id
)
return attempt.id
......@@ -496,17 +505,6 @@ def get_student_view(user_id, course_id, content_id,
if user_role != 'student':
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
exam_id = None
......@@ -519,18 +517,37 @@ def get_student_view(user_id, course_id, content_id,
return None
exam_id = exam['id']
is_proctored = exam['is_proctored']
except ProctoredExamNotFoundException:
# This really shouldn't happen
# as Studio will be setting this up
is_proctored = context.get('is_proctored', False)
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=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)
has_started_exam = attempt and attempt.get('started_at')
......@@ -555,7 +572,10 @@ def get_student_view(user_id, course_id, content_id,
if is_proctored:
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:
provider = get_backend_provider()
student_view_template = 'proctoring/seq_proctored_exam_instructions.html'
......@@ -568,7 +588,10 @@ def get_student_view(user_id, course_id, content_id,
elif attempt['status'] == ProctoredExamStudentAttemptStatus.timed_out:
student_view_template = 'proctoring/seq_timed_exam_expired.html'
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:
student_view_template = 'proctoring/seq_proctored_exam_verified.html'
elif attempt['status'] == ProctoredExamStudentAttemptStatus.rejected:
......@@ -597,6 +620,7 @@ def get_student_view(user_id, course_id, content_id,
'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,
'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
'exam_started_poll_url': reverse(
'edx_proctoring.proctored_exam.attempt',
......
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'ProctoredExam.is_practice_exam'
db.add_column('proctoring_proctoredexam', 'is_practice_exam',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'ProctoredExam.is_practice_exam'
db.delete_column('proctoring_proctoredexam', 'is_practice_exam')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'edx_proctoring.proctoredexam': {
'Meta': {'unique_together': "(('course_id', 'content_id'),)", 'object_name': 'ProctoredExam', 'db_table': "'proctoring_proctoredexam'"},
'content_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'exam_name': ('django.db.models.fields.TextField', [], {}),
'external_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_practice_exam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_proctored': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'time_limit_mins': ('django.db.models.fields.IntegerField', [], {})
},
'edx_proctoring.proctoredexamsoftwaresecurecomment': {
'Meta': {'object_name': 'ProctoredExamSoftwareSecureComment', 'db_table': "'proctoring_proctoredexamstudentattemptcomment'"},
'comment': ('django.db.models.fields.TextField', [], {}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'duration': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'review': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExamSoftwareSecureReview']"}),
'start_time': ('django.db.models.fields.IntegerField', [], {}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'stop_time': ('django.db.models.fields.IntegerField', [], {})
},
'edx_proctoring.proctoredexamsoftwaresecurereview': {
'Meta': {'object_name': 'ProctoredExamSoftwareSecureReview', 'db_table': "'proctoring_proctoredexamsoftwaresecurereview'"},
'attempt_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'raw_data': ('django.db.models.fields.TextField', [], {}),
'review_status': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'video_url': ('django.db.models.fields.TextField', [], {})
},
'edx_proctoring.proctoredexamstudentallowance': {
'Meta': {'unique_together': "(('user', 'proctored_exam', 'key'),)", 'object_name': 'ProctoredExamStudentAllowance', 'db_table': "'proctoring_proctoredexamstudentallowance'"},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'edx_proctoring.proctoredexamstudentallowancehistory': {
'Meta': {'object_name': 'ProctoredExamStudentAllowanceHistory', 'db_table': "'proctoring_proctoredexamstudentallowancehistory'"},
'allowance_id': ('django.db.models.fields.IntegerField', [], {}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'edx_proctoring.proctoredexamstudentattempt': {
'Meta': {'unique_together': "(('user', 'proctored_exam'),)", 'object_name': 'ProctoredExamStudentAttempt', 'db_table': "'proctoring_proctoredexamstudentattempt'"},
'allowed_time_limit_mins': ('django.db.models.fields.IntegerField', [], {}),
'attempt_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'completed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'external_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_sample_attempt': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_poll_ipaddr': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'last_poll_timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'started_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'student_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'taking_as_proctored': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'edx_proctoring.proctoredexamstudentattempthistory': {
'Meta': {'object_name': 'ProctoredExamStudentAttemptHistory'},
'allowed_time_limit_mins': ('django.db.models.fields.IntegerField', [], {}),
'attempt_code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'completed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'external_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_sample_attempt': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proctored_exam': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['edx_proctoring.ProctoredExam']"}),
'started_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'student_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'taking_as_proctored': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['edx_proctoring']
\ No newline at end of file
......@@ -37,6 +37,9 @@ class ProctoredExam(TimeStampedModel):
# Whether this exam actually is proctored or not.
is_proctored = models.BooleanField()
# Whether this exam is for practice only.
is_practice_exam = models.BooleanField()
# Whether this exam will be active.
is_active = models.BooleanField()
......@@ -118,7 +121,7 @@ class ProctoredExamStudentAttemptManager(models.Manager):
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):
"""
......@@ -128,7 +131,7 @@ class ProctoredExamStudentAttemptManager(models.Manager):
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):
"""
......@@ -219,7 +222,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# in case there is an option to opt-out
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
is_sample_attempt = models.BooleanField()
......@@ -266,7 +269,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
def delete_exam_attempt(self):
"""
deletes the exam attempt object.
deletes the exam attempt object and archives it to the ProctoredExamStudentAttemptHistory table.
"""
self.delete()
......
......@@ -29,6 +29,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
time_limit_mins = serializers.IntegerField(required=True)
is_active = StrictBooleanField(required=True)
is_practice_exam = StrictBooleanField(required=True)
is_proctored = StrictBooleanField(required=True)
class Meta:
......@@ -39,7 +40,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
fields = (
"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):
self.course_id = 'test_course'
self.content_id = 'test_content_id'
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.exam_name = 'Test Exam'
self.user_id = self.user.id
......@@ -72,6 +73,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.external_id = 'test_external_id'
self.proctored_exam_id = self._create_proctored_exam()
self.timed_exam = self._create_timed_exam()
self.practice_exam_id = self._create_practice_exam()
self.disabled_exam_id = self._create_disabled_exam()
# Messages for get_student_view
......@@ -85,6 +87,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
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.practice_exam_submitted_msg = 'You have submitted this practice proctored exam'
def _create_proctored_exam(self):
"""
......@@ -109,6 +113,19 @@ class ProctoredExamApiTests(LoggedInTestCase):
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):
"""
Calls the api's create_exam to create an exam object.
......@@ -145,6 +162,20 @@ class ProctoredExamApiTests(LoggedInTestCase):
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):
"""
creates allowance for user.
......@@ -173,6 +204,24 @@ class ProctoredExamApiTests(LoggedInTestCase):
with self.assertRaises(ProctoredExamAlreadyExists):
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):
"""
test update the existing proctored exam
......@@ -217,7 +266,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertEqual(proctored_exam['exam_name'], self.exam_name)
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):
"""
......@@ -320,7 +369,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
proctored_exam_student_attempt = self._create_unstarted_exam_attempt()
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):
"""
......@@ -473,8 +530,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
filtered_attempts = get_filtered_exam_attempts(self.course_id, self.user.username)
self.assertEqual(len(filtered_attempts), 2)
self.assertEqual(filtered_attempts[0]['id'], exam_attempt.id)
self.assertEqual(filtered_attempts[1]['id'], new_exam_attempt)
self.assertEqual(filtered_attempts[0]['id'], new_exam_attempt)
self.assertEqual(filtered_attempts[1]['id'], exam_attempt.id)
def test_get_all_exam_attempts(self):
"""
......@@ -493,8 +550,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
all_exams = get_all_exam_attempts(self.course_id)
self.assertEqual(len(all_exams), 2)
self.assertEqual(all_exams[0]['id'], exam_attempt.id)
self.assertEqual(all_exams[1]['id'], updated_exam_attempt_id)
self.assertEqual(all_exams[0]['id'], updated_exam_attempt_id)
self.assertEqual(all_exams[1]['id'], exam_attempt.id)
def test_get_student_view(self):
"""
......@@ -514,9 +571,45 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertIn('data-exam-id="%d"' % self.proctored_exam_id, 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
"""
rendered_response = get_student_view(
......@@ -529,7 +622,8 @@ class ProctoredExamApiTests(LoggedInTestCase):
'default_time_limit_mins': 90,
'credit_state': {
'enrollment_mode': 'honor'
}
},
'is_practice_exam': False
}
)
self.assertIsNone(rendered_response)
......@@ -629,6 +723,22 @@ 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
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
"""
Test for get_student_view proctored exam which has been rejected.
......
......@@ -22,11 +22,15 @@ class TestProctoredExamSerializer(unittest.TestCase):
'time_limit_mins': 90,
'external_id': '123',
'is_proctored': 'bla',
'is_practice_exam': 'bla',
'is_active': 'f'
}
serializer = ProctoredExamSerializer(data=data)
self.assertFalse(serializer.is_valid())
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):
'time_limit_mins': 90,
'external_id': '123',
'is_proctored': True,
'is_practice_exam': False,
'is_active': True
}
response = self.client.post(
......@@ -119,6 +120,7 @@ class ProctoredExamViewTests(LoggedInTestCase):
'time_limit_mins': 90,
'external_id': '123',
'is_proctored': True,
'is_practice_exam': False,
'is_active': True
}
response = self.client.post(
......
......@@ -81,6 +81,7 @@ class ProctoredExamView(AuthenticatedAPIView):
"exam_name": "Midterm",
"time_limit_mins": 90,
"is_proctored": true,
"is_practice_exam": false,
"external_id": "12213DASAD",
"is_active": true
}
......@@ -91,6 +92,7 @@ class ProctoredExamView(AuthenticatedAPIView):
* 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.
* 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.
* is_active: Whether this exam will be active.
......@@ -108,6 +110,7 @@ class ProctoredExamView(AuthenticatedAPIView):
"exam_name": "Final",
"time_limit_mins": 120,
"is_proctored": true,
"is_practice_exam": false,
"external_id": 235
"is_active": true
}
......@@ -140,6 +143,7 @@ class ProctoredExamView(AuthenticatedAPIView):
exam_name=request.DATA.get('exam_name', None),
time_limit_mins=request.DATA.get('time_limit_mins', 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),
is_active=request.DATA.get('is_active', None)
)
......@@ -162,6 +166,7 @@ class ProctoredExamView(AuthenticatedAPIView):
exam_name=request.DATA.get('exam_name', None),
time_limit_mins=request.DATA.get('time_limit_mins', 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),
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