Commit 5b074962 by Chris Dodge

Add status to attempts

parent f004c530
...@@ -24,6 +24,7 @@ from edx_proctoring.models import ( ...@@ -24,6 +24,7 @@ from edx_proctoring.models import (
ProctoredExam, ProctoredExam,
ProctoredExamStudentAllowance, ProctoredExamStudentAllowance,
ProctoredExamStudentAttempt, ProctoredExamStudentAttempt,
ProctoredExamStudentAttemptStatus,
) )
from edx_proctoring.serializers import ( from edx_proctoring.serializers import (
ProctoredExamSerializer, ProctoredExamSerializer,
...@@ -308,9 +309,23 @@ def stop_exam_attempt(exam_id, user_id): ...@@ -308,9 +309,23 @@ def stop_exam_attempt(exam_id, user_id):
""" """
exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id) exam_attempt_obj = ProctoredExamStudentAttempt.objects.get_exam_attempt(exam_id, user_id)
if exam_attempt_obj is None: 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: else:
exam_attempt_obj.completed_at = datetime.now(pytz.UTC) 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() exam_attempt_obj.save()
return exam_attempt_obj.id return exam_attempt_obj.id
...@@ -419,7 +434,7 @@ def get_active_exams_for_user(user_id, course_id=None): ...@@ -419,7 +434,7 @@ def get_active_exams_for_user(user_id, course_id=None):
return result 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 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 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): ...@@ -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']) expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins'])
has_time_expired = now_utc > expires_at 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: if not has_started_exam:
# determine whether to show a timed exam only entrance screen # determine whether to show a timed exam only entrance screen
# or a screen regarding proctoring # or a screen regarding proctoring
...@@ -483,7 +502,6 @@ def get_student_view(user_id, course_id, content_id, context): ...@@ -483,7 +502,6 @@ def get_student_view(user_id, course_id, content_id, context):
student_view_template = 'proctoring/seq_timed_exam_completed.html' student_view_template = 'proctoring/seq_timed_exam_completed.html'
elif has_time_expired: elif has_time_expired:
student_view_template = 'proctoring/seq_timed_exam_expired.html' student_view_template = 'proctoring/seq_timed_exam_expired.html'
if student_view_template: if student_view_template:
template = loader.get_template(student_view_template) template = loader.get_template(student_view_template)
django_context = Context(context) django_context = Context(context)
......
...@@ -141,6 +141,49 @@ class ProctoredExamStudentAttemptManager(models.Manager): ...@@ -141,6 +141,49 @@ class ProctoredExamStudentAttemptManager(models.Manager):
return self.filter(filtered_query) 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): class ProctoredExamStudentAttempt(TimeStampedModel):
""" """
Information about the Student Attempt on a Information about the Student Attempt on a
...@@ -205,7 +248,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -205,7 +248,8 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
attempt_code=attempt_code, attempt_code=attempt_code,
taking_as_proctored=taking_as_proctored, taking_as_proctored=taking_as_proctored,
is_sample_attempt=is_sample_attempt, is_sample_attempt=is_sample_attempt,
external_id=external_id external_id=external_id,
status=ProctoredExamStudentAttemptStatus.created,
) )
def start_exam_attempt(self): def start_exam_attempt(self):
...@@ -213,6 +257,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel): ...@@ -213,6 +257,7 @@ class ProctoredExamStudentAttempt(TimeStampedModel):
sets the model's state when an exam attempt has started sets the model's state when an exam attempt has started
""" """
self.started_at = datetime.now(pytz.UTC) self.started_at = datetime.now(pytz.UTC)
self.status = ProctoredExamStudentAttemptStatus.started
self.save() self.save()
def delete_exam_attempt(self): def delete_exam_attempt(self):
......
var edx = edx || {}; var edx = edx || {};
(function (Backbone, $, _) { (function (Backbone, $, _, gettext) {
'use strict'; 'use strict';
edx.instructor_dashboard = edx.instructor_dashboard || {}; edx.instructor_dashboard = edx.instructor_dashboard || {};
...@@ -11,7 +11,7 @@ var edx = edx || {}; ...@@ -11,7 +11,7 @@ var edx = edx || {};
return new Date(date).toString('MMM dd, yyyy h:mmtt'); return new Date(date).toString('MMM dd, yyyy h:mmtt');
} }
else { else {
return 'N/A'; return '---';
} }
} }
...@@ -130,6 +130,11 @@ var edx = edx || {}; ...@@ -130,6 +130,11 @@ var edx = edx || {};
}, },
onRemoveAttempt: function (event) { onRemoveAttempt: function (event) {
event.preventDefault(); 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 $target = $(event.currentTarget);
var attemptId = $target.data("attemptId"); var attemptId = $target.data("attemptId");
...@@ -148,4 +153,4 @@ var edx = edx || {}; ...@@ -148,4 +153,4 @@ var edx = edx || {};
} }
}); });
this.edx.instructor_dashboard.proctoring.ProctoredExamAttemptView = edx.instructor_dashboard.proctoring.ProctoredExamAttemptView; this.edx.instructor_dashboard.proctoring.ProctoredExamAttemptView = edx.instructor_dashboard.proctoring.ProctoredExamAttemptView;
}).call(this, Backbone, $, _); }).call(this, Backbone, $, _, gettext);
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<section class="content"> <section class="content">
<div class="top-header"> <div class="top-header">
<div class='search-attempts'> <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) { %> <% if (inSearchMode) { %>
value="<%= searchText %>" value="<%= searchText %>"
<%} %> <%} %>
......
...@@ -27,6 +27,7 @@ from edx_proctoring.api import ( ...@@ -27,6 +27,7 @@ from edx_proctoring.api import (
get_all_exam_attempts, get_all_exam_attempts,
get_filtered_exam_attempts, get_filtered_exam_attempts,
is_feature_enabled, is_feature_enabled,
mark_exam_attempt_timeout,
) )
from edx_proctoring.exceptions import ( from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists, ProctoredExamAlreadyExists,
...@@ -373,6 +374,21 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -373,6 +374,21 @@ class ProctoredExamApiTests(LoggedInTestCase):
with self.assertRaises(StudentExamAttemptDoesNotExistsException): with self.assertRaises(StudentExamAttemptDoesNotExistsException):
stop_exam_attempt(self.proctored_exam_id, self.user_id) 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): def test_get_active_exams_for_user(self):
""" """
Test to get the all the active 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