Commit 44b442d7 by chrisndodge

Merge pull request #59 from edx/cdodge/add-exam-controls

add ability to stop an exam
parents a8ba1176 97728159
...@@ -189,7 +189,8 @@ class ProctoredExamStudentAttemptManager(models.Manager): ...@@ -189,7 +189,8 @@ class ProctoredExamStudentAttemptManager(models.Manager):
""" """
Returns the active student exams (user in-progress exams) Returns the active student exams (user in-progress exams)
""" """
filtered_query = Q(user_id=user_id) & Q(status=ProctoredExamStudentAttemptStatus.started) filtered_query = Q(user_id=user_id) & (Q(status=ProctoredExamStudentAttemptStatus.started) |
Q(status=ProctoredExamStudentAttemptStatus.ready_to_submit))
if course_id is not None: if course_id is not None:
filtered_query = filtered_query & Q(proctored_exam__course_id=course_id) filtered_query = filtered_query & Q(proctored_exam__course_id=course_id)
......
...@@ -2,6 +2,7 @@ $(function() { ...@@ -2,6 +2,7 @@ $(function() {
var proctored_exam_view = new edx.coursware.proctored_exam.ProctoredExamView({ var proctored_exam_view = new edx.coursware.proctored_exam.ProctoredExamView({
el: $(".proctored_exam_status"), el: $(".proctored_exam_status"),
proctored_template: '#proctored-exam-status-tpl', proctored_template: '#proctored-exam-status-tpl',
controls_template: '#proctored-exam-controls-tpl',
model: new ProctoredExamModel() model: new ProctoredExamModel()
}); });
proctored_exam_view.render(); proctored_exam_view.render();
......
...@@ -125,7 +125,7 @@ var edx = edx || {}; ...@@ -125,7 +125,7 @@ var edx = edx || {};
_.extend(data, viewHelper); _.extend(data, viewHelper);
var html = this.template(data); var html = this.template(data);
this.$el.html(html); this.$el.html(html);
} }
}, },
onRemoveAttempt: function (event) { onRemoveAttempt: function (event) {
event.preventDefault(); event.preventDefault();
......
...@@ -11,6 +11,7 @@ var edx = edx || {}; ...@@ -11,6 +11,7 @@ var edx = edx || {};
this.$el = options.el; this.$el = options.el;
this.model = options.model; this.model = options.model;
this.templateId = options.proctored_template; this.templateId = options.proctored_template;
this.examControlsTemplateId = options.controls_template;
this.template = null; this.template = null;
this.timerId = null; this.timerId = null;
this.timerTick = 0; this.timerTick = 0;
...@@ -26,6 +27,13 @@ var edx = edx || {}; ...@@ -26,6 +27,13 @@ var edx = edx || {};
/* don't assume this backbone view is running on a page with the underscore templates */ /* don't assume this backbone view is running on a page with the underscore templates */
this.template = _.template(template_html); this.template = _.template(template_html);
} }
var controls_template_html = $(this.examControlsTemplateId).text();
if (controls_template_html !== null) {
/* don't assume this backbone view is running on a page with the underscore templates */
this.controls_template = _.template(controls_template_html);
}
/* re-render if the model changes */ /* re-render if the model changes */
this.listenTo(this.model, 'change', this.modelChanged); this.listenTo(this.model, 'change', this.modelChanged);
...@@ -41,9 +49,10 @@ var edx = edx || {}; ...@@ -41,9 +49,10 @@ var edx = edx || {};
// should not be navigating around the courseware // should not be navigating around the courseware
var taking_as_proctored = this.model.get('taking_as_proctored'); var taking_as_proctored = this.model.get('taking_as_proctored');
var time_left = this.model.get('time_remaining_seconds') > 0; var time_left = this.model.get('time_remaining_seconds') > 0;
var status = this.model.get('attempt_status');
var in_courseware = document.location.href.indexOf('/courses/' + this.model.get('course_id') + '/courseware/') > -1; var in_courseware = document.location.href.indexOf('/courses/' + this.model.get('course_id') + '/courseware/') > -1;
if ( taking_as_proctored && time_left && in_courseware){ if ( taking_as_proctored && time_left && in_courseware && status !== 'started'){
$(window).bind('beforeunload', this.unloadMessage); $(window).bind('beforeunload', this.unloadMessage);
} else { } else {
// remove callback on unload event // remove callback on unload event
...@@ -64,11 +73,36 @@ var edx = edx || {}; ...@@ -64,11 +73,36 @@ var edx = edx || {};
this.$el.show(); this.$el.show();
this.updateRemainingTime(this); this.updateRemainingTime(this);
this.timerId = setInterval(this.updateRemainingTime, 1000, this); this.timerId = setInterval(this.updateRemainingTime, 1000, this);
// put the exam controls (namely stop button)
// right after the sequence naviation ribbon
html = this.controls_template(this.model.toJSON());
$('.sequence-nav').after(html);
// Bind a click handler to the exam controls
var self = this;
$('.proctored-exam-action-stop').click(function(){
$(window).unbind('beforeunload', self.unloadMessage);
$.ajax({
url: '/api/edx_proctoring/v1/proctored_exam/attempt/' + self.model.get('attempt_id'),
type: 'PUT',
data: {
action: 'stop'
},
success: function() {
// Reloading page will reflect the new state of the attempt
location.reload();
}
});
});
$('.proctored-exam-action-stop').css('cursor', 'pointer');
} }
} }
return this; return this;
}, },
unloadMessage: function () { unloadMessage: function () {
return null;
return gettext("As you are currently taking a proctored exam,\n" + return gettext("As you are currently taking a proctored exam,\n" +
"you should not be navigation away from the exam.\n" + "you should not be navigation away from the exam.\n" +
"This may be considered as a violation of the \n" + "This may be considered as a violation of the \n" +
...@@ -77,7 +111,7 @@ var edx = edx || {}; ...@@ -77,7 +111,7 @@ var edx = edx || {};
}, },
updateRemainingTime: function (self) { updateRemainingTime: function (self) {
self.timerTick ++; self.timerTick ++;
if (self.timerTick % 5 == 0){ if (self.timerTick % 5 === 0){
var url = self.model.url + '/' + self.model.get('attempt_id'); var url = self.model.url + '/' + self.model.get('attempt_id');
$.ajax(url).success(function(data) { $.ajax(url).success(function(data) {
if (data.status === 'error') { if (data.status === 'error') {
......
...@@ -39,9 +39,9 @@ ...@@ -39,9 +39,9 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<p class="proctored-exam-instruction"> <p class="proctored-exam-instruction">
{% blocktrans %} {% trans "Don't want to take this exam with online proctoring?" %}<a class="proctored-decline-exam" href='#' data-action="decline" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}">
Don't want to take this exam with online proctoring? <a href="#">Take this exam as an open exam instead.</a> {% trans "Take this exam as an open exam instead." %}
{% endblocktrans %} </a>
</p> </p>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctoring/seq_proctored_exam_footer.html' %}
...@@ -50,6 +50,27 @@ ...@@ -50,6 +50,27 @@
var _waiting_for_proctored_interval = null; var _waiting_for_proctored_interval = null;
$('.proctored-decline-exam').click(
function(event) {
var action_url = $(this).data('change-state-url');
var exam_id = $(this).data('exam-id');
var action = $(this).data('action')
// Update the state of the attempt
$.ajax({
url: action_url,
type: 'PUT',
data: {
action: action
},
success: function() {
// Reloading page will reflect the new state of the attempt
location.reload()
}
});
}
);
$(document).ready(function(){ $(document).ready(function(){
var hasFlash = false; var hasFlash = false;
......
...@@ -4,6 +4,7 @@ All tests for the proctored_exams.py ...@@ -4,6 +4,7 @@ All tests for the proctored_exams.py
""" """
import json import json
import pytz import pytz
import ddt
from mock import Mock from mock import Mock
from freezegun import freeze_time from freezegun import freeze_time
from httmock import HTTMock from httmock import HTTMock
...@@ -376,6 +377,7 @@ class ProctoredExamViewTests(LoggedInTestCase): ...@@ -376,6 +377,7 @@ class ProctoredExamViewTests(LoggedInTestCase):
self.assertEqual(response_data['time_limit_mins'], proctored_exam.time_limit_mins) self.assertEqual(response_data['time_limit_mins'], proctored_exam.time_limit_mins)
@ddt.ddt
class TestStudentProctoredExamAttempt(LoggedInTestCase): class TestStudentProctoredExamAttempt(LoggedInTestCase):
""" """
Tests for the StudentProctoredExamAttempt Tests for the StudentProctoredExamAttempt
...@@ -711,7 +713,12 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -711,7 +713,12 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data['exam_attempt_id'], old_attempt_id) self.assertEqual(response_data['exam_attempt_id'], old_attempt_id)
def test_submit_exam_attempt(self): @ddt.data(
('submit', ProctoredExamStudentAttemptStatus.submitted),
('decline', ProctoredExamStudentAttemptStatus.declined)
)
@ddt.unpack
def test_submit_exam_attempt(self, action, expected_status):
""" """
Tries to submit an exam Tries to submit an exam
""" """
...@@ -741,7 +748,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -741,7 +748,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response = self.client.put( response = self.client.put(
reverse('edx_proctoring.proctored_exam.attempt', args=[old_attempt_id]), reverse('edx_proctoring.proctored_exam.attempt', args=[old_attempt_id]),
{ {
'action': 'submit', 'action': action,
} }
) )
...@@ -751,7 +758,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -751,7 +758,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
attempt = get_exam_attempt_by_id(response_data['exam_attempt_id']) attempt = get_exam_attempt_by_id(response_data['exam_attempt_id'])
self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.submitted) self.assertEqual(attempt['status'], expected_status)
# we should not be able to restart it # we should not be able to restart it
response = self.client.put( response = self.client.put(
......
...@@ -172,7 +172,8 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -172,7 +172,8 @@ class ProctoredExamView(AuthenticatedAPIView):
is_active=request.DATA.get('is_active', None), is_active=request.DATA.get('is_active', None),
) )
return Response({'exam_id': exam_id}) return Response({'exam_id': exam_id})
except ProctoredExamNotFoundException: except ProctoredExamNotFoundException, ex:
LOG.exception(ex)
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam_id does not exist."} data={"detail": "The exam_id does not exist."}
...@@ -193,7 +194,8 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -193,7 +194,8 @@ class ProctoredExamView(AuthenticatedAPIView):
data=get_exam_by_id(exam_id), data=get_exam_by_id(exam_id),
status=status.HTTP_200_OK status=status.HTTP_200_OK
) )
except ProctoredExamNotFoundException: except ProctoredExamNotFoundException, ex:
LOG.exception(ex)
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam_id does not exist."} data={"detail": "The exam_id does not exist."}
...@@ -207,7 +209,8 @@ class ProctoredExamView(AuthenticatedAPIView): ...@@ -207,7 +209,8 @@ class ProctoredExamView(AuthenticatedAPIView):
data=get_exam_by_content_id(course_id, content_id), data=get_exam_by_content_id(course_id, content_id),
status=status.HTTP_200_OK status=status.HTTP_200_OK
) )
except ProctoredExamNotFoundException: except ProctoredExamNotFoundException, ex:
LOG.exception(ex)
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam with course_id, content_id does not exist."} data={"detail": "The exam with course_id, content_id does not exist."}
...@@ -294,6 +297,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -294,6 +297,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
) )
except ProctoredBaseException, ex: except ProctoredBaseException, ex:
LOG.exception(ex)
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": str(ex)} data={"detail": str(ex)}
...@@ -343,6 +347,12 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -343,6 +347,12 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
request.user.id, request.user.id,
ProctoredExamStudentAttemptStatus.submitted ProctoredExamStudentAttemptStatus.submitted
) )
elif action == 'decline':
exam_attempt_id = update_attempt_status(
attempt['proctored_exam']['id'],
request.user.id,
ProctoredExamStudentAttemptStatus.declined
)
return Response({"exam_attempt_id": exam_attempt_id}) return Response({"exam_attempt_id": exam_attempt_id})
except ProctoredBaseException, ex: except ProctoredBaseException, ex:
...@@ -373,6 +383,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -373,6 +383,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
return Response() return Response()
except ProctoredBaseException, ex: except ProctoredBaseException, ex:
LOG.exception(ex)
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": str(ex)} data={"detail": str(ex)}
...@@ -556,6 +567,7 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView): ...@@ -556,6 +567,7 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
return Response({'exam_attempt_id': exam_attempt_id}) return Response({'exam_attempt_id': exam_attempt_id})
except ProctoredBaseException, ex: except ProctoredBaseException, ex:
LOG.exception(ex)
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": unicode(ex)} data={"detail": unicode(ex)}
...@@ -626,6 +638,7 @@ class ExamAllowanceView(AuthenticatedAPIView): ...@@ -626,6 +638,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
)) ))
except UserNotFoundException, ex: except UserNotFoundException, ex:
LOG.exception(ex)
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
data={"detail": str(ex)} 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