Commit 5b074962 by Chris Dodge

Add status to attempts

parent f004c530
......@@ -24,6 +24,7 @@ from edx_proctoring.models import (
ProctoredExam,
ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus,
)
from edx_proctoring.serializers import (
ProctoredExamSerializer,
......@@ -308,9 +309,23 @@ def stop_exam_attempt(exam_id, user_id):
"""
exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None:
raise StudentExamAttemptDoesNotExistsException('Error. Trying to stop an exam that is not in progress.')
raise StudentExamAttemptDoesNotExistsException('Error. Trying to stop an exam that does not exist.')
else:
exam_attempt_obj.completed_at = datetime.now(pytz.UTC)
exam_attempt_obj.status = ProctoredExamStudentAttemptStatus.completed
exam_attempt_obj.save()
return exam_attempt_obj.id
def mark_exam_attempt_timeout(exam_id, user_id):
"""
Marks the exam attempt as timed_out
"""
exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None:
raise StudentExamAttemptDoesNotExistsException('Error. Trying to time out an exam that does not exist.')
else:
exam_attempt_obj.status = ProctoredExamStudentAttemptStatus.timed_out
exam_attempt_obj.save()
return exam_attempt_obj.id
......@@ -419,7 +434,7 @@ def get_active_exams_for_user(user_id, course_id=None):
return result
def get_student_view(user_id, course_id, content_id, context):
def get_student_view(user_id, course_id, content_id, context): # pylint: disable=too-many-branches
"""
Helper method that will return the view HTML related to the exam control
flow (i.e. entering, expired, completed, etc.) If there is no specific
......@@ -463,6 +478,10 @@ def get_student_view(user_id, course_id, content_id, context):
expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins'])
has_time_expired = now_utc > expires_at
# make sure the attempt has been marked as timed_out, if need be
if has_time_expired and attempt['status'] != ProctoredExamStudentAttemptStatus.timed_out:
mark_exam_attempt_timeout(exam_id, user_id)
if not has_started_exam:
# determine whether to show a timed exam only entrance screen
# or a screen regarding proctoring
......@@ -483,7 +502,6 @@ def get_student_view(user_id, course_id, content_id, context):
student_view_template = 'proctoring/seq_timed_exam_completed.html'
elif has_time_expired:
student_view_template = 'proctoring/seq_timed_exam_expired.html'
if student_view_template:
template = loader.get_template(student_view_template)
django_context = Context(context)
......
......@@ -141,6 +141,49 @@ class ProctoredExamStudentAttemptManager(models.Manager):
return self.filter(filtered_query)
class ProctoredExamStudentAttemptStatus(object):
"""
A class to enumerate the various status that an attempt can have
IMPORTANT: Since these values are stored in a database, they are system
constants and should not be language translated, since translations
might change over time.
"""
# the student is eligible to decide if he/she wants to persue credit
eligible = 'Eligible'
# the attempt record has been created, but the exam has not yet
# been started
created = 'Created'
# the attempt is ready to start but requires
# user to acknowledge that he/she wants to start the exam
ready_to_start = 'Ready to start'
# the student has started the exam and is
# in the process of completing the exam
started = 'Started'
# the exam has timed out
timed_out = 'Timed Out'
# the student has completed the exam
completed = 'Completed'
# the student has submitted the exam for proctoring review
submitted = 'Submitted'
# the exam has been verified and approved
verified = 'Verified'
# the exam has been rejected
rejected = 'Rejected'
# the exam is believed to be in error
error = 'Error'
class ProctoredExamStudentAttempt(TimeStampedModel):
"""
Information about the Student Attempt on a
......@@ -205,7 +248,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
attempt_code=attempt_code,
taking_as_proctored=taking_as_proctored,
is_sample_attempt=is_sample_attempt,
external_id=external_id
external_id=external_id,
status=ProctoredExamStudentAttemptStatus.created,
)
def start_exam_attempt(self):
......@@ -213,6 +257,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
sets the model's state when an exam attempt has started
"""
self.started_at = datetime.now(pytz.UTC)
self.status = ProctoredExamStudentAttemptStatus.started
self.save()
def delete_exam_attempt(self):
......
var edx = edx || {};
(function (Backbone, $, _) {
(function (Backbone, $, _, gettext) {
'use strict';
edx.instructor_dashboard = edx.instructor_dashboard || {};
......@@ -11,7 +11,7 @@ var edx = edx || {};
return new Date(date).toString('MMM dd, yyyy h:mmtt');
}
else {
return 'N/A';
return '---';
}
}
......@@ -130,6 +130,11 @@ var edx = edx || {};
},
onRemoveAttempt: function (event) {
event.preventDefault();
// confirm the user's intent
if (!confirm(gettext('Are you sure you wish to remove this student\'s exam attempt?'))) {
return;
}
var $target = $(event.currentTarget);
var attemptId = $target.data("attemptId");
......@@ -148,4 +153,4 @@ var edx = edx || {};
}
});
this.edx.instructor_dashboard.proctoring.ProctoredExamAttemptView = edx.instructor_dashboard.proctoring.ProctoredExamAttemptView;
}).call(this, Backbone, $, _);
}).call(this, Backbone, $, _, gettext);
......@@ -2,7 +2,7 @@
<section class="content">
<div class="top-header">
<div class='search-attempts'>
<input type="text" id="search_attempt_id" placeholder="e.g johndoe or john.do@gmail.com"
<input type="text" id="search_attempt_id" placeholder="e.g johndoe or john.doe@gmail.com"
<% if (inSearchMode) { %>
value="<%= searchText %>"
<%} %>
......@@ -118,4 +118,4 @@
</tbody>
</table>
</section>
</div>
\ No newline at end of file
</div>
......@@ -27,6 +27,7 @@ from edx_proctoring.api import (
get_all_exam_attempts,
get_filtered_exam_attempts,
is_feature_enabled,
mark_exam_attempt_timeout,
)
from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists,
......@@ -373,6 +374,21 @@ class ProctoredExamApiTests(LoggedInTestCase):
with self.assertRaises(StudentExamAttemptDoesNotExistsException):
stop_exam_attempt(self.proctored_exam_id, self.user_id)
def test_mark_exam_attempt_timeout(self):
"""
Tests the mark exam as timed out
"""
with self.assertRaises(StudentExamAttemptDoesNotExistsException):
mark_exam_attempt_timeout(self.proctored_exam_id, self.user_id)
proctored_exam_student_attempt = self._create_unstarted_exam_attempt()
self.assertIsNone(proctored_exam_student_attempt.completed_at)
proctored_exam_attempt_id = mark_exam_attempt_timeout(
proctored_exam_student_attempt.proctored_exam, self.user_id
)
self.assertEqual(proctored_exam_student_attempt.id, proctored_exam_attempt_id)
def test_get_active_exams_for_user(self):
"""
Test to get the all the active
......
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