Commit 923f51b1 by chrisndodge

Merge pull request #12 from edx/muhhshoaib/PHX-43-implement-proctored-start-page

PHX-43 Implemented proctored start page
parents d539a957 17ef189f
...@@ -12,13 +12,22 @@ from django.template import Context, loader ...@@ -12,13 +12,22 @@ from django.template import Context, loader
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamNotFoundException, StudentExamAttemptAlreadyExistsException, ProctoredExamAlreadyExists,
StudentExamAttemptDoesNotExistsException) ProctoredExamNotFoundException,
StudentExamAttemptAlreadyExistsException,
StudentExamAttemptDoesNotExistsException,
StudentExamAttemptedAlreadyStarted,
)
from edx_proctoring.models import ( from edx_proctoring.models import (
ProctoredExam, ProctoredExamStudentAllowance, ProctoredExamStudentAttempt ProctoredExam,
ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt,
)
from edx_proctoring.serializers import (
ProctoredExamSerializer,
ProctoredExamStudentAttemptSerializer,
ProctoredExamStudentAllowanceSerializer,
) )
from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAttemptSerializer, \
ProctoredExamStudentAllowanceSerializer
from edx_proctoring.utils import humanized_time from edx_proctoring.utils import humanized_time
...@@ -139,31 +148,70 @@ def get_exam_attempt(exam_id, user_id): ...@@ -139,31 +148,70 @@ def get_exam_attempt(exam_id, user_id):
""" """
Return an existing exam attempt for the given student Return an existing exam attempt for the given student
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.get_student_exam_attempt(exam_id, user_id) exam_attempt_obj = ProctoredExamStudentAttempt.get_exam_attempt(exam_id, user_id)
return exam_attempt_obj.__dict__ if exam_attempt_obj else None return exam_attempt_obj.__dict__ if exam_attempt_obj else None
def start_exam_attempt(exam_id, user_id, external_id): def create_exam_attempt(exam_id, user_id, external_id):
"""
Creates an exam attempt for user_id against exam_id. There should only be
one exam_attempt per user per exam. Multiple attempts by user will be archived
in a separate table
"""
if ProctoredExamStudentAttempt.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)
attempt = ProctoredExamStudentAttempt.create_exam_attempt(
exam_id,
user_id,
'', # student name is TBD
external_id
)
return attempt.id
def start_exam_attempt(exam_id, user_id):
""" """
Signals the beginning of an exam attempt for a given Signals the beginning of an exam attempt for a given
exam_id. If one already exists, then an exception should be thrown. exam_id. If one already exists, then an exception should be thrown.
Returns: exam_attempt_id (PK) Returns: exam_attempt_id (PK)
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.start_exam_attempt(exam_id, user_id, external_id)
if exam_attempt_obj is None: existing_attempt = ProctoredExamStudentAttempt.get_exam_attempt(exam_id, user_id)
raise StudentExamAttemptAlreadyExistsException
else: if not existing_attempt:
return exam_attempt_obj.id err_msg = (
'Cannot start exam attempt for exam_id = {exam_id} '
'and user_id = {user_id} because it does not exist!'
).format(exam_id=exam_id, user_id=user_id)
raise StudentExamAttemptDoesNotExistsException(err_msg)
if existing_attempt.started_at:
# cannot restart an attempt
err_msg = (
'Cannot start exam attempt for exam_id = {exam_id} '
'and user_id = {user_id} because it has already started!'
).format(exam_id=exam_id, user_id=user_id)
raise StudentExamAttemptedAlreadyStarted(err_msg)
existing_attempt.start_exam_attempt()
def stop_exam_attempt(exam_id, user_id): def stop_exam_attempt(exam_id, user_id):
""" """
Marks the exam attempt as completed (sets the completed_at field and updates the record) Marks the exam attempt as completed (sets the completed_at field and updates the record)
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.get_student_exam_attempt(exam_id, user_id) exam_attempt_obj = ProctoredExamStudentAttempt.get_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None: if exam_attempt_obj is None:
raise StudentExamAttemptDoesNotExistsException raise StudentExamAttemptDoesNotExistsException('Error. Trying to stop an exam that is not in progress.')
else: else:
exam_attempt_obj.completed_at = datetime.now(pytz.UTC) exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
exam_attempt_obj.save() exam_attempt_obj.save()
...@@ -191,7 +239,7 @@ def get_active_exams_for_user(user_id, course_id=None): ...@@ -191,7 +239,7 @@ def get_active_exams_for_user(user_id, course_id=None):
""" """
result = [] result = []
student_active_exams = ProctoredExamStudentAttempt.get_active_student_exams(user_id, course_id) student_active_exams = ProctoredExamStudentAttempt.get_active_student_attempts(user_id, course_id)
for active_exam in student_active_exams: for active_exam in student_active_exams:
# convert the django orm objects # convert the django orm objects
# into the serialized form. # into the serialized form.
...@@ -241,17 +289,20 @@ def get_student_view(user_id, course_id, content_id, context): ...@@ -241,17 +289,20 @@ def get_student_view(user_id, course_id, content_id, context):
) )
attempt = get_exam_attempt(exam_id, user_id) attempt = get_exam_attempt(exam_id, user_id)
has_started_exam = attempt is not None has_started_exam = attempt and attempt.get('started_at')
if attempt: if has_started_exam:
now_utc = datetime.now(pytz.UTC) now_utc = datetime.now(pytz.UTC)
expires_at = attempt['started_at'] + timedelta(minutes=context['default_time_limit_mins']) expires_at = attempt['started_at'] + timedelta(minutes=context['default_time_limit_mins'])
has_time_expired = now_utc > expires_at has_time_expired = now_utc > expires_at
if not has_started_exam: if not has_started_exam:
# determine whether to show a timed exam only entrace screen # determine whether to show a timed exam only entrance screen
# or a screen regarding proctoring # or a screen regarding proctoring
if is_proctored: if is_proctored:
student_view_template = 'proctoring/seq_proctored_exam_entrance.html' if not attempt:
student_view_template = 'proctoring/seq_proctored_exam_entrance.html'
else:
student_view_template = 'proctoring/seq_proctored_exam_instructions.html'
else: else:
student_view_template = 'proctoring/seq_timed_exam_entrance.html' student_view_template = 'proctoring/seq_timed_exam_entrance.html'
elif has_finished_exam: elif has_finished_exam:
......
...@@ -3,25 +3,37 @@ Specialized exceptions for the Notification subsystem ...@@ -3,25 +3,37 @@ Specialized exceptions for the Notification subsystem
""" """
class ProctoredExamAlreadyExists(Exception): class ProctoredBaseException(Exception):
"""
A common base class for all exceptions
"""
class ProctoredExamAlreadyExists(ProctoredBaseException):
""" """
Raised when trying to create an Exam that already exists. Raised when trying to create an Exam that already exists.
""" """
class ProctoredExamNotFoundException(Exception): class ProctoredExamNotFoundException(ProctoredBaseException):
""" """
Raised when a look up fails. Raised when a look up fails.
""" """
class StudentExamAttemptAlreadyExistsException(Exception): class StudentExamAttemptAlreadyExistsException(ProctoredBaseException):
""" """
Raised when trying to start an exam when an Exam Attempt already exists. Raised when trying to start an exam when an Exam Attempt already exists.
""" """
class StudentExamAttemptDoesNotExistsException(Exception): class StudentExamAttemptDoesNotExistsException(ProctoredBaseException):
""" """
Raised when trying to stop an exam attempt where the Exam Attempt doesn't exist. Raised when trying to stop an exam attempt where the Exam Attempt doesn't exist.
""" """
class StudentExamAttemptedAlreadyStarted(ProctoredBaseException):
"""
Raised when the same exam attempt is being started twice
"""
# -*- 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 'ProctoredExamStudentAttempt.taking_as_proctored'
db.add_column('proctoring_proctoredexamstudentattempt', 'taking_as_proctored',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'ProctoredExamStudentAttempt.taking_as_proctored'
db.delete_column('proctoring_proctoredexamstudentattempt', 'taking_as_proctored')
models = {
'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_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.proctoredexamstudentallowance': {
'Meta': {'unique_together': "(('user_id', '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_id': ('django.db.models.fields.IntegerField', [], {}),
'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_id': ('django.db.models.fields.IntegerField', [], {}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'edx_proctoring.proctoredexamstudentattempt': {
'Meta': {'object_name': 'ProctoredExamStudentAttempt', 'db_table': "'proctoring_proctoredexamstudentattempt'"},
'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'}),
'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'}),
'taking_as_proctored': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'})
}
}
complete_apps = ['edx_proctoring']
\ No newline at end of file
# -*- 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 'ProctoredExamStudentAttempt.student_name'
db.add_column('proctoring_proctoredexamstudentattempt', 'student_name',
self.gf('django.db.models.fields.CharField')(max_length=255, null=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'ProctoredExamStudentAttempt.student_name'
db.delete_column('proctoring_proctoredexamstudentattempt', 'student_name')
models = {
'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_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.proctoredexamstudentallowance': {
'Meta': {'unique_together': "(('user_id', '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_id': ('django.db.models.fields.IntegerField', [], {}),
'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_id': ('django.db.models.fields.IntegerField', [], {}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'edx_proctoring.proctoredexamstudentattempt': {
'Meta': {'object_name': 'ProctoredExamStudentAttempt', 'db_table': "'proctoring_proctoredexamstudentattempt'"},
'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'}),
'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', 'null': 'True'}),
'taking_as_proctored': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'})
}
}
complete_apps = ['edx_proctoring']
\ No newline at end of file
...@@ -85,6 +85,12 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -85,6 +85,12 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
# what is the status of this attempt # what is the status of this attempt
status = models.CharField(max_length=64) status = models.CharField(max_length=64)
# if the user is attempting this as a proctored exam
# in case there is an option to opt-out
taking_as_proctored = models.BooleanField()
student_name = models.CharField(max_length=255)
class Meta: class Meta:
""" Meta class for this Django model """ """ Meta class for this Django model """
db_table = 'proctoring_proctoredexamstudentattempt' db_table = 'proctoring_proctoredexamstudentattempt'
...@@ -96,23 +102,29 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -96,23 +102,29 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return self.started_at and not self.completed_at return self.started_at and not self.completed_at
@classmethod @classmethod
def start_exam_attempt(cls, exam_id, user_id, external_id): def create_exam_attempt(cls, exam_id, user_id, student_name, external_id):
""" """
create and return an exam attempt entry for a given Create a new exam attempt entry for a given exam_id and
exam_id. If one already exists, then returns None. user_id.
""" """
if cls.get_student_exam_attempt(exam_id, user_id) is None:
return cls.objects.create( return cls.objects.create(
proctored_exam_id=exam_id, proctored_exam_id=exam_id,
user_id=user_id, user_id=user_id,
external_id=external_id, student_name=student_name,
started_at=datetime.now(pytz.UTC) external_id=external_id
) )
else:
return None def start_exam_attempt(self):
"""
sets the model's state when an exam attempt has started
"""
self.started_at = datetime.now(pytz.UTC)
self.save()
@classmethod @classmethod
def get_student_exam_attempt(cls, exam_id, user_id): def get_exam_attempt(cls, exam_id, user_id):
""" """
Returns the Student Exam Attempt object if found Returns the Student Exam Attempt object if found
else Returns None. else Returns None.
...@@ -124,7 +136,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -124,7 +136,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return exam_attempt_obj return exam_attempt_obj
@classmethod @classmethod
def get_active_student_exams(cls, user_id, course_id=None): def get_active_student_attempts(cls, user_id, course_id=None):
""" """
Returns the active student exams (user in-progress exams) Returns the active student exams (user in-progress exams)
""" """
......
...@@ -17,10 +17,14 @@ ...@@ -17,10 +17,14 @@
var currentTime = (new Date()).getTime(); var currentTime = (new Date()).getTime();
var lastFetched = this.get('lastFetched').getTime(); var lastFetched = this.get('lastFetched').getTime();
var totalSeconds = this.get('time_remaining_seconds') - (currentTime - lastFetched) / 1000; var totalSeconds = this.get('time_remaining_seconds') - (currentTime - lastFetched) / 1000;
return (totalSeconds > 0) ? totalSeconds : 0; return totalSeconds;
}, },
getFormattedRemainingTime: function () { getFormattedRemainingTime: function () {
var totalSeconds = this.getRemainingSeconds(); var totalSeconds = this.getRemainingSeconds();
/* since we can have a small grace period, we can end in the negative numbers */
if (totalSeconds < 0)
totalSeconds = 0;
var hours = parseInt(totalSeconds / 3600) % 24; var hours = parseInt(totalSeconds / 3600) % 24;
var minutes = parseInt(totalSeconds / 60) % 60; var minutes = parseInt(totalSeconds / 60) % 60;
var seconds = Math.floor(totalSeconds % 60); var seconds = Math.floor(totalSeconds % 60);
......
...@@ -13,6 +13,8 @@ var edx = edx || {}; ...@@ -13,6 +13,8 @@ var edx = edx || {};
this.templateId = options.proctored_template; this.templateId = options.proctored_template;
this.template = null; this.template = null;
this.timerId = null; this.timerId = null;
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this.grace_period_secs = 5;
var template_html = $(this.templateId).text(); var template_html = $(this.templateId).text();
if (template_html !== null) { if (template_html !== null) {
...@@ -47,7 +49,7 @@ var edx = edx || {}; ...@@ -47,7 +49,7 @@ var edx = edx || {};
self.$el.find('div.exam-timer').removeClass("low-time warning critical"); self.$el.find('div.exam-timer').removeClass("low-time warning critical");
self.$el.find('div.exam-timer').addClass(self.model.getRemainingTimeState()); self.$el.find('div.exam-timer').addClass(self.model.getRemainingTimeState());
self.$el.find('span#time_remaining_id b').html(self.model.getFormattedRemainingTime()); self.$el.find('span#time_remaining_id b').html(self.model.getFormattedRemainingTime());
if (self.model.getRemainingSeconds() <= 0) { if (self.model.getRemainingSeconds() <= -self.grace_period_secs) {
clearInterval(self.timerId); // stop the timer once the time finishes. clearInterval(self.timerId); // stop the timer once the time finishes.
// refresh the page when the timer expired // refresh the page when the timer expired
location.reload(); location.reload();
......
{% load i18n %} {% load i18n %}
<div class="sequence" data-exam-id="{{exam_id}}"> <div class="sequence" data-exam-id="{{exam_id}}">
This is to be developed!
</div> </div>
{% include 'proctoring/seq_timed_exam_footer.html' %} <div class="sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
Would you Like to take {{ display_name }} as Proctored Exam?
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
Since you're enrolled in this course as a verified student, you have the option to take this exam
with online proctoring. Online proctoring is one requirement towards being eligible for credit.
{% 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>
{% trans "Yes, take this exam as a proctored exam (and be eligible for credit)" %}
</a>
<p>
{% blocktrans %}
You will need to <strong>download and install edX-approved software </strong>for the online proctoring
of your exam. After successful installation, you will be <strong>guided through setting up your
proctored session and begin the exam immediately </strong>afterwards.</p>
{% endblocktrans %}
<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></i>
</div>
<div class="gated-sequence">
<span><i class="fa fa-unlock"></i></span>
<a class="start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=false>
{% trans "No, take this exam as an open exam (and not be eligible for credit)" %}
</a>
<p>
{% blocktrans %}
You may proceed and begin the exam at your leisure, but <strong>you will not be able to apply for college
credit </strong>upon completing the exam or this course in general.
{% 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=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');
if (typeof action_url === "undefined" ) {
return false;
}
$.post(
action_url,
{
"exam_id": exam_id,
"attempt_proctored": attempt_proctored,
"start_clock": false
},
function(data) {
// reload the page, because we've unlocked it
location.reload();
}
);
}
);
</script>
{% load i18n %}
<div class="footer-sequence">
<h4> {% trans "Why i am seeing these options?" %} </h4>
<p>
{% blocktrans %}
Text to be added here.
{% endblocktrans %}
</p>
</div>
<div class="faq-proctoring-exam">
<h4> {% trans "See Also" %} </h4>
<p>
{% blocktrans %}
<a class="footer-link" href="#">
Frequently asked questions about proctoring and earning college credit.
</a>
<a class="footer-link" href="#">
Technical Requirements for taking a proctored exam
</a>
{% endblocktrans %}
</p>
</div>
\ No newline at end of file
{% load i18n %}
<div class="sequence" data-exam-id="{{exam_id}}">
How to launch the proctored exam content goes here
</div>
...@@ -10,9 +10,7 @@ ...@@ -10,9 +10,7 @@
<strong> <strong>
{% trans "In order to successfully pass this exam you will have to answer the following questions and problems in the time allotted." %} {% trans "In order to successfully pass this exam you will have to answer the following questions and problems in the time allotted." %}
</strong> </strong>
{% blocktrans %} {% trans "Once you proceed, you'll start both the exam and the "%} {{total_time|lower}} {% trans " given to you." %}
Once you proceed, you'll start both the exam and the {{total_time|lower}} given to you.
{% endblocktrans %}
</p> </p>
<div class="gated-sequence"> <div class="gated-sequence">
<a class='start-timed-exam' data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}"> <a class='start-timed-exam' data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}">
...@@ -34,7 +32,8 @@ ...@@ -34,7 +32,8 @@
$.post( $.post(
action_url, action_url,
{ {
"exam_id": exam_id "exam_id": exam_id,
"start_clock": true
}, },
function(data) { function(data) {
// reload the page, because we've unlocked it // reload the page, because we've unlocked it
......
...@@ -13,7 +13,8 @@ from edx_proctoring.api import ( ...@@ -13,7 +13,8 @@ from edx_proctoring.api import (
start_exam_attempt, start_exam_attempt,
stop_exam_attempt, stop_exam_attempt,
get_active_exams_for_user, get_active_exams_for_user,
get_exam_attempt get_exam_attempt,
create_exam_attempt
) )
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamAlreadyExists,
...@@ -187,11 +188,11 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -187,11 +188,11 @@ class ProctoredExamApiTests(LoggedInTestCase):
remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key) remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key)
self.assertEqual(len(ProctoredExamStudentAllowance.objects.filter()), 0) self.assertEqual(len(ProctoredExamStudentAllowance.objects.filter()), 0)
def test_start_an_exam_attempt(self): def test_create_an_exam_attempt(self):
""" """
Start an exam attempt. Start an exam attempt.
""" """
attempt_id = start_exam_attempt(self.proctored_exam_id, self.user_id, self.external_id) attempt_id = create_exam_attempt(self.proctored_exam_id, self.user_id, '')
self.assertGreater(attempt_id, 0) self.assertGreater(attempt_id, 0)
def test_get_exam_attempt(self): def test_get_exam_attempt(self):
...@@ -211,7 +212,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -211,7 +212,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
""" """
proctored_exam_student_attempt = self._create_student_exam_attempt() proctored_exam_student_attempt = self._create_student_exam_attempt()
with self.assertRaises(StudentExamAttemptAlreadyExistsException): with self.assertRaises(StudentExamAttemptAlreadyExistsException):
start_exam_attempt(proctored_exam_student_attempt.proctored_exam, self.user_id, self.external_id) create_exam_attempt(proctored_exam_student_attempt.proctored_exam, self.user_id, self.external_id)
def test_stop_exam_attempt(self): def test_stop_exam_attempt(self):
""" """
...@@ -244,11 +245,15 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -244,11 +245,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_name='Final Test Exam', exam_name='Final Test Exam',
time_limit_mins=self.default_time_limit time_limit_mins=self.default_time_limit
) )
start_exam_attempt( create_exam_attempt(
exam_id=exam_id, exam_id=exam_id,
user_id=self.user_id, user_id=self.user_id,
external_id=self.external_id external_id=self.external_id
) )
start_exam_attempt(
exam_id=exam_id,
user_id=self.user_id,
)
add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value) add_allowance_for_user(self.proctored_exam_id, self.user_id, self.key, self.value)
add_allowance_for_user(self.proctored_exam_id, self.user_id, 'new_key', 'new_value') add_allowance_for_user(self.proctored_exam_id, self.user_id, 'new_key', 'new_value')
student_active_exams = get_active_exams_for_user(self.user_id, self.course_id) student_active_exams = get_active_exams_for_user(self.user_id, self.course_id)
......
...@@ -350,8 +350,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -350,8 +350,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
) )
attempt_data = { attempt_data = {
'exam_id': proctored_exam.id, 'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id, 'external_id': proctored_exam.external_id,
'external_id': proctored_exam.external_id 'start_clock': True,
} }
response = self.client.post( response = self.client.post(
reverse('edx_proctoring.proctored_exam.attempt'), reverse('edx_proctoring.proctored_exam.attempt'),
...@@ -376,7 +376,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -376,7 +376,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
) )
attempt_data = { attempt_data = {
'exam_id': proctored_exam.id, 'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id,
'external_id': proctored_exam.external_id 'external_id': proctored_exam.external_id
} }
response = self.client.post( response = self.client.post(
...@@ -394,7 +393,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -394,7 +393,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data['detail'], 'Error. Trying to start an exam that has already started.') self.assertEqual(
response_data['detail'],
'Cannot create new exam attempt for exam_id = 1 and user_id = 1 because it already exists!'
)
def test_stop_exam_attempt(self): def test_stop_exam_attempt(self):
""" """
......
...@@ -19,10 +19,13 @@ from edx_proctoring.api import ( ...@@ -19,10 +19,13 @@ from edx_proctoring.api import (
stop_exam_attempt, stop_exam_attempt,
add_allowance_for_user, add_allowance_for_user,
remove_allowance_for_user, remove_allowance_for_user,
get_active_exams_for_user get_active_exams_for_user,
create_exam_attempt
)
from edx_proctoring.exceptions import (
ProctoredBaseException,
ProctoredExamNotFoundException,
) )
from edx_proctoring.exceptions import ProctoredExamNotFoundException, \
StudentExamAttemptAlreadyExistsException, StudentExamAttemptDoesNotExistsException
from edx_proctoring.serializers import ProctoredExamSerializer from edx_proctoring.serializers import ProctoredExamSerializer
from .utils import AuthenticatedAPIView from .utils import AuthenticatedAPIView
...@@ -236,20 +239,26 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -236,20 +239,26 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
def post(self, request): def post(self, request):
""" """
HTTP POST handler. To start an exam. HTTP POST handler. To create an exam.
""" """
start_immediately = request.DATA.get('start_clock', 'false').lower() == 'true'
exam_id = request.DATA.get('exam_id', None)
try: try:
exam_attempt_id = start_exam_attempt( exam_attempt_id = create_exam_attempt(
exam_id=request.DATA.get('exam_id', None), exam_id=exam_id,
user_id=request.user.id, user_id=request.user.id,
external_id=request.DATA.get('external_id', None) external_id=request.DATA.get('external_id', None),
) )
if start_immediately:
start_exam_attempt(exam_id, request.user.id)
return Response({'exam_attempt_id': exam_attempt_id}) return Response({'exam_attempt_id': exam_attempt_id})
except StudentExamAttemptAlreadyExistsException: except ProctoredBaseException, ex:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "Error. Trying to start an exam that has already started."} data={"detail": str(ex)}
) )
def put(self, request): def put(self, request):
...@@ -263,10 +272,10 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -263,10 +272,10 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
) )
return Response({"exam_attempt_id": exam_attempt_id}) return Response({"exam_attempt_id": exam_attempt_id})
except StudentExamAttemptDoesNotExistsException: except ProctoredBaseException, ex:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "Error. Trying to stop an exam that is not in progress."} data={"detail": str(ex)}
) )
......
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