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):
"""
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:
filtered_query = filtered_query & Q(proctored_exam__course_id=course_id)
......
......@@ -2,6 +2,7 @@ $(function() {
var proctored_exam_view = new edx.coursware.proctored_exam.ProctoredExamView({
el: $(".proctored_exam_status"),
proctored_template: '#proctored-exam-status-tpl',
controls_template: '#proctored-exam-controls-tpl',
model: new ProctoredExamModel()
});
proctored_exam_view.render();
......
......@@ -125,7 +125,7 @@ var edx = edx || {};
_.extend(data, viewHelper);
var html = this.template(data);
this.$el.html(html);
}
}
},
onRemoveAttempt: function (event) {
event.preventDefault();
......
......@@ -11,6 +11,7 @@ var edx = edx || {};
this.$el = options.el;
this.model = options.model;
this.templateId = options.proctored_template;
this.examControlsTemplateId = options.controls_template;
this.template = null;
this.timerId = null;
this.timerTick = 0;
......@@ -26,6 +27,13 @@ var edx = edx || {};
/* don't assume this backbone view is running on a page with the underscore templates */
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 */
this.listenTo(this.model, 'change', this.modelChanged);
......@@ -41,9 +49,10 @@ var edx = edx || {};
// should not be navigating around the courseware
var taking_as_proctored = this.model.get('taking_as_proctored');
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;
if ( taking_as_proctored && time_left && in_courseware){
if ( taking_as_proctored && time_left && in_courseware && status !== 'started'){
$(window).bind('beforeunload', this.unloadMessage);
} else {
// remove callback on unload event
......@@ -64,11 +73,36 @@ var edx = edx || {};
this.$el.show();
this.updateRemainingTime(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;
},
unloadMessage: function () {
return null;
return gettext("As you are currently taking a proctored exam,\n" +
"you should not be navigation away from the exam.\n" +
"This may be considered as a violation of the \n" +
......@@ -77,7 +111,7 @@ var edx = edx || {};
},
updateRemainingTime: function (self) {
self.timerTick ++;
if (self.timerTick % 5 == 0){
if (self.timerTick % 5 === 0){
var url = self.model.url + '/' + self.model.get('attempt_id');
$.ajax(url).success(function(data) {
if (data.status === 'error') {
......
......@@ -39,9 +39,9 @@
{% endblocktrans %}
</p>
<p class="proctored-exam-instruction">
{% blocktrans %}
Don't want to take this exam with online proctoring? <a href="#">Take this exam as an open exam instead.</a>
{% endblocktrans %}
{% 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}}">
{% trans "Take this exam as an open exam instead." %}
</a>
</p>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
......@@ -50,6 +50,27 @@
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(){
var hasFlash = false;
......
......@@ -4,6 +4,7 @@ All tests for the proctored_exams.py
"""
import json
import pytz
import ddt
from mock import Mock
from freezegun import freeze_time
from httmock import HTTMock
......@@ -376,6 +377,7 @@ class ProctoredExamViewTests(LoggedInTestCase):
self.assertEqual(response_data['time_limit_mins'], proctored_exam.time_limit_mins)
@ddt.ddt
class TestStudentProctoredExamAttempt(LoggedInTestCase):
"""
Tests for the StudentProctoredExamAttempt
......@@ -711,7 +713,12 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response_data = json.loads(response.content)
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
"""
......@@ -741,7 +748,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response = self.client.put(
reverse('edx_proctoring.proctored_exam.attempt', args=[old_attempt_id]),
{
'action': 'submit',
'action': action,
}
)
......@@ -751,7 +758,7 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
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
response = self.client.put(
......
......@@ -172,7 +172,8 @@ class ProctoredExamView(AuthenticatedAPIView):
is_active=request.DATA.get('is_active', None),
)
return Response({'exam_id': exam_id})
except ProctoredExamNotFoundException:
except ProctoredExamNotFoundException, ex:
LOG.exception(ex)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam_id does not exist."}
......@@ -193,7 +194,8 @@ class ProctoredExamView(AuthenticatedAPIView):
data=get_exam_by_id(exam_id),
status=status.HTTP_200_OK
)
except ProctoredExamNotFoundException:
except ProctoredExamNotFoundException, ex:
LOG.exception(ex)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam_id does not exist."}
......@@ -207,7 +209,8 @@ class ProctoredExamView(AuthenticatedAPIView):
data=get_exam_by_content_id(course_id, content_id),
status=status.HTTP_200_OK
)
except ProctoredExamNotFoundException:
except ProctoredExamNotFoundException, ex:
LOG.exception(ex)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": "The exam with course_id, content_id does not exist."}
......@@ -294,6 +297,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
)
except ProctoredBaseException, ex:
LOG.exception(ex)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": str(ex)}
......@@ -343,6 +347,12 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
request.user.id,
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})
except ProctoredBaseException, ex:
......@@ -373,6 +383,7 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
return Response()
except ProctoredBaseException, ex:
LOG.exception(ex)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": str(ex)}
......@@ -556,6 +567,7 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
return Response({'exam_attempt_id': exam_attempt_id})
except ProctoredBaseException, ex:
LOG.exception(ex)
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"detail": unicode(ex)}
......@@ -626,6 +638,7 @@ class ExamAllowanceView(AuthenticatedAPIView):
))
except UserNotFoundException, ex:
LOG.exception(ex)
return Response(
status=status.HTTP_400_BAD_REQUEST,
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