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
from django.core.urlresolvers import reverse
from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamNotFoundException, StudentExamAttemptAlreadyExistsException,
StudentExamAttemptDoesNotExistsException)
ProctoredExamAlreadyExists,
ProctoredExamNotFoundException,
StudentExamAttemptAlreadyExistsException,
StudentExamAttemptDoesNotExistsException,
StudentExamAttemptedAlreadyStarted,
)
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
......@@ -139,31 +148,70 @@ def get_exam_attempt(exam_id, user_id):
"""
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
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
exam_id. If one already exists, then an exception should be thrown.
Returns: exam_attempt_id (PK)
"""
exam_attempt_obj = ProctoredExamStudentAttempt.start_exam_attempt(exam_id, user_id, external_id)
if exam_attempt_obj is None:
raise StudentExamAttemptAlreadyExistsException
else:
return exam_attempt_obj.id
existing_attempt = ProctoredExamStudentAttempt.get_exam_attempt(exam_id, user_id)
if not existing_attempt:
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):
"""
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:
raise StudentExamAttemptDoesNotExistsException
raise StudentExamAttemptDoesNotExistsException('Error. Trying to stop an exam that is not in progress.')
else:
exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
exam_attempt_obj.save()
......@@ -191,7 +239,7 @@ def get_active_exams_for_user(user_id, course_id=None):
"""
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:
# convert the django orm objects
# into the serialized form.
......@@ -241,17 +289,20 @@ def get_student_view(user_id, course_id, content_id, context):
)
attempt = get_exam_attempt(exam_id, user_id)
has_started_exam = attempt is not None
if attempt:
has_started_exam = attempt and attempt.get('started_at')
if has_started_exam:
now_utc = datetime.now(pytz.UTC)
expires_at = attempt['started_at'] + timedelta(minutes=context['default_time_limit_mins'])
has_time_expired = now_utc > expires_at
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
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:
student_view_template = 'proctoring/seq_timed_exam_entrance.html'
elif has_finished_exam:
......
......@@ -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.
"""
class ProctoredExamNotFoundException(Exception):
class ProctoredExamNotFoundException(ProctoredBaseException):
"""
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.
"""
class StudentExamAttemptDoesNotExistsException(Exception):
class StudentExamAttemptDoesNotExistsException(ProctoredBaseException):
"""
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):
# what is the status of this attempt
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:
""" Meta class for this Django model """
db_table = 'proctoring_proctoredexamstudentattempt'
......@@ -96,23 +102,29 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return self.started_at and not self.completed_at
@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
exam_id. If one already exists, then returns None.
Create a new exam attempt entry for a given exam_id and
user_id.
"""
if cls.get_student_exam_attempt(exam_id, user_id) is None:
return cls.objects.create(
proctored_exam_id=exam_id,
user_id=user_id,
external_id=external_id,
started_at=datetime.now(pytz.UTC)
)
else:
return None
return cls.objects.create(
proctored_exam_id=exam_id,
user_id=user_id,
student_name=student_name,
external_id=external_id
)
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
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
else Returns None.
......@@ -124,7 +136,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
return exam_attempt_obj
@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)
"""
......
......@@ -17,10 +17,14 @@
var currentTime = (new Date()).getTime();
var lastFetched = this.get('lastFetched').getTime();
var totalSeconds = this.get('time_remaining_seconds') - (currentTime - lastFetched) / 1000;
return (totalSeconds > 0) ? totalSeconds : 0;
return totalSeconds;
},
getFormattedRemainingTime: function () {
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 minutes = parseInt(totalSeconds / 60) % 60;
var seconds = Math.floor(totalSeconds % 60);
......
......@@ -13,6 +13,8 @@ var edx = edx || {};
this.templateId = options.proctored_template;
this.template = 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();
if (template_html !== null) {
......@@ -47,7 +49,7 @@ var edx = edx || {};
self.$el.find('div.exam-timer').removeClass("low-time warning critical");
self.$el.find('div.exam-timer').addClass(self.model.getRemainingTimeState());
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.
// refresh the page when the timer expired
location.reload();
......
{% load i18n %}
<div class="sequence" data-exam-id="{{exam_id}}">
This is to be developed!
</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 @@
<strong>
{% trans "In order to successfully pass this exam you will have to answer the following questions and problems in the time allotted." %}
</strong>
{% blocktrans %}
Once you proceed, you'll start both the exam and the {{total_time|lower}} given to you.
{% endblocktrans %}
{% trans "Once you proceed, you'll start both the exam and the "%} {{total_time|lower}} {% trans " given to you." %}
</p>
<div class="gated-sequence">
<a class='start-timed-exam' data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}">
......@@ -34,7 +32,8 @@
$.post(
action_url,
{
"exam_id": exam_id
"exam_id": exam_id,
"start_clock": true
},
function(data) {
// reload the page, because we've unlocked it
......
......@@ -13,7 +13,8 @@ from edx_proctoring.api import (
start_exam_attempt,
stop_exam_attempt,
get_active_exams_for_user,
get_exam_attempt
get_exam_attempt,
create_exam_attempt
)
from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists,
......@@ -187,11 +188,11 @@ class ProctoredExamApiTests(LoggedInTestCase):
remove_allowance_for_user(student_allowance.proctored_exam.id, self.user_id, self.key)
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.
"""
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)
def test_get_exam_attempt(self):
......@@ -211,7 +212,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
"""
proctored_exam_student_attempt = self._create_student_exam_attempt()
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):
"""
......@@ -244,11 +245,15 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_name='Final Test Exam',
time_limit_mins=self.default_time_limit
)
start_exam_attempt(
create_exam_attempt(
exam_id=exam_id,
user_id=self.user_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, 'new_key', 'new_value')
student_active_exams = get_active_exams_for_user(self.user_id, self.course_id)
......
......@@ -350,8 +350,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
attempt_data = {
'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(
reverse('edx_proctoring.proctored_exam.attempt'),
......@@ -376,7 +376,6 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
attempt_data = {
'exam_id': proctored_exam.id,
'user_id': self.student_taking_exam.id,
'external_id': proctored_exam.external_id
}
response = self.client.post(
......@@ -394,7 +393,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
)
self.assertEqual(response.status_code, 400)
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):
"""
......
......@@ -19,10 +19,13 @@ from edx_proctoring.api import (
stop_exam_attempt,
add_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 .utils import AuthenticatedAPIView
......@@ -236,20 +239,26 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
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:
exam_attempt_id = start_exam_attempt(
exam_id=request.DATA.get('exam_id', None),
exam_attempt_id = create_exam_attempt(
exam_id=exam_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})
except StudentExamAttemptAlreadyExistsException:
except ProctoredBaseException, ex:
return Response(
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):
......@@ -263,10 +272,10 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
)
return Response({"exam_attempt_id": exam_attempt_id})
except StudentExamAttemptDoesNotExistsException:
except ProctoredBaseException, ex:
return Response(
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