Commit edc4d2a1 by chrisndodge

Merge pull request #27 from edx/cdodge/punchlist1

Cdodge/punchlist1
parents c272b6d8 1b1a309f
......@@ -2,6 +2,6 @@ edx-proctoring [![Build Status](https://travis-ci.org/edx/edx-proctoring.svg?bra
========================
This is the Exam Proctoring subsytem for the Open edX platform.
This is the Exam Proctoring subsytsem for the Open edX platform.
This is a work in progress at this point in time.
......@@ -11,7 +11,7 @@ import uuid
from datetime import datetime, timedelta
from django.conf import settings
from django.template import Context, loader
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse, NoReverseMatch
from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists,
......@@ -446,10 +446,20 @@ def get_student_view(user_id, course_id, content_id, context):
template = loader.get_template(student_view_template)
django_context = Context(context)
total_time = humanized_time(context['default_time_limit_mins'])
progress_page_url = ''
try:
progress_page_url = reverse(
'courseware.views.progress',
args=[course_id]
)
except NoReverseMatch:
pass
django_context.update({
'platform_name': settings.PLATFORM_NAME,
'total_time': total_time,
'exam_id': exam_id,
'progress_page_url': progress_page_url,
'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
'exam_started_poll_url': reverse(
'edx_proctoring.proctored_exam.attempt',
......
......@@ -78,7 +78,7 @@ class ProctoredExamStudentAttemptSerializer(serializers.ModelSerializer):
fields = (
"id", "created", "modified", "user_id", "started_at", "completed_at",
"external_id", "status", "proctored_exam_id", "allowed_time_limit_mins",
"attempt_code", "is_sample_attempt"
"attempt_code", "is_sample_attempt", "taking_as_proctored"
)
......
......@@ -5,12 +5,12 @@
defaults: {
in_timed_exam: false,
is_proctored: false,
taking_as_proctored: false,
exam_display_name: '',
exam_url_path: '',
time_remaining_seconds: 0,
low_threshold: 0,
critically_low_threshold: 0,
low_threshold_sec: 0,
critically_low_threshold_sec: 0,
lastFetched: new Date()
},
getRemainingSeconds: function () {
......@@ -35,10 +35,10 @@
},
getRemainingTimeState: function () {
var totalSeconds = this.getRemainingSeconds();
if (totalSeconds > this.get('low_threshold')) {
if (totalSeconds > this.get('low_threshold_sec')) {
return "";
}
else if (totalSeconds <= this.get('low_threshold') && totalSeconds > this.get('critically_low_threshold')) {
else if (totalSeconds <= this.get('low_threshold_sec') && totalSeconds > this.get('critically_low_threshold_sec')) {
return "low-time warning";
}
else {
......
......@@ -16,6 +16,10 @@ var edx = edx || {};
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this.grace_period_secs = 5;
// we need to keep a copy here because the model will
// get destroyed before onbeforeunload is called
this.taking_as_proctored = false;
var template_html = $(this.templateId).text();
if (template_html !== null) {
/* don't assume this backbone view is running on a page with the underscore templates */
......@@ -24,12 +28,24 @@ var edx = edx || {};
/* re-render if the model changes */
this.listenTo(this.model, 'change', this.modelChanged);
$(window).unbind('beforeunload', this.unloadMessage);
/* make the async call to the backend REST API */
/* after it loads, the listenTo event will file and */
/* will call into the rendering */
this.model.fetch();
},
modelChanged: function () {
// if we are a proctored exam, then we need to alert user that he/she
// should not leave the exam
if (this.model.get('taking_as_proctored') && this.model.get('time_remaining_seconds') > 0) {
$(window).bind('beforeunload', this.unloadMessage);
} else {
// remove callback on unload event
$(window).unbind('beforeunload', this.unloadMessage);
}
this.render();
},
render: function () {
......@@ -44,13 +60,20 @@ var edx = edx || {};
}
return this;
},
unloadMessage: function () {
return "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" +
"proctored exam and you may be disqualified for \n" +
"credit eligibility in this course.\n";
},
updateRemainingTime: function (self) {
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() <= -self.grace_period_secs) {
clearInterval(self.timerId); // stop the timer once the time finishes.
$(window).unbind('beforeunload', this.unloadMessage);
// refresh the page when the timer expired
location.reload();
}
......
......@@ -17,7 +17,7 @@
Here is your unique exam code. You'll be asked for it during the setup.
{% endblocktrans %}
</h3>
<h1> {{exam_code}}</h1>
<h2> {{exam_code}}<span class='copy-to-clipboard'></span></h2>
<p>
{% blocktrans %}
Please do not share this code. It can only be used once and it tied to your {{platform_name}} account.
......@@ -43,6 +43,48 @@
<script type="text/javascript">
$(document).ready(function(){
var hasFlash = false;
try {
var fo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
if (fo) {
hasFlash = true;
}
} catch (e) {
if (navigator.mimeTypes
&& navigator.mimeTypes['application/x-shockwave-flash'] != undefined
&& navigator.mimeTypes['application/x-shockwave-flash'].enabledPlugin) {
hasFlash = true;
}
}
if (hasFlash) {
$('.copy-to-clipboard').html(
'<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" \
width="110" \
height="14" \
id="clippy" > \
<param name="movie" value="/static/proctoring/clippy.swf"/> \
<param name="allowScriptAccess" value="always" /> \
<param name="quality" value="high" /> \
<param name="scale" value="noscale" /> \
<param NAME="FlashVars" value="text={{exam_code}}"> \
<param name="bgcolor" value="#F2F4F5"> \
<embed src="/static/proctoring/clippy.swf" \
width="110" \
height="14" \
name="clippy" \
quality="high" \
allowScriptAccess="always" \
type="application/x-shockwave-flash" \
pluginspage="http://www.macromedia.com/go/getflashplayer" \
FlashVars="text={{exam_code}}" \
bgcolor="#F2F4F5" \
/> \
</object>'
);
}
setInterval(
poll_exam_started,
5000
......@@ -53,6 +95,12 @@
var url = $('.instructions').data('exam-started-poll-url')
$.ajax(url).success(function(data){
if (data.started_at !== null) {
// Let the student know exam has started and clock is running.
// this may or may not bring the browser window back to the
// foreground (depending on browser as well as user settings)
alert('{% trans "Your proctored exam has started, please click OK to enter into your exam." %}')
// Reloading page will reflect the new state of the attempt
location.reload()
}
});
......
......@@ -15,7 +15,7 @@
<div class="proctored-exam-message">
<p>
{% blocktrans %}
Please see <a href="#">your progress in this course</a> for your general course credit worthiness.
Please see <a href="{{progress_page_url}}">your progress in this course</a> for your general course credit worthiness.
{% endblocktrans %}
</p>
</div>
......
......@@ -633,6 +633,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(data['exam_display_name'], 'Test Exam')
self.assertEqual(data['low_threshold_sec'], 1080)
self.assertEqual(data['critically_low_threshold_sec'], 270)
def test_get_expired_attempt(self):
"""
......
......@@ -7,6 +7,8 @@ import pytz
from datetime import datetime, timedelta
from django.utils.decorators import method_decorator
from django.conf import settings
from django.core.urlresolvers import reverse, NoReverseMatch
from rest_framework import status
from rest_framework.response import Response
......@@ -364,10 +366,13 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
exams = get_active_exams_for_user(request.user.id)
if exams:
exam = exams[0]
exam_info = exams[0]
exam = exam_info['exam']
attempt = exam_info['attempt']
# need to adjust for allowances
expires_at = exam['attempt']['started_at'] + timedelta(minutes=exam['attempt']['allowed_time_limit_mins'])
expires_at = attempt['started_at'] + timedelta(minutes=attempt['allowed_time_limit_mins'])
now_utc = datetime.now(pytz.UTC)
if expires_at > now_utc:
......@@ -375,14 +380,34 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
else:
time_remaining_seconds = 0
proctoring_settings = getattr(settings, 'PROCTORING_SETTINGS', {})
low_threshold_pct = proctoring_settings.get('low_threshold_pct', .2)
critically_low_threshold_pct = proctoring_settings.get('critically_low_threshold_pct', .05)
low_threshold = int(low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60)
critically_low_threshold = int(
critically_low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60
)
exam_url_path = ''
try:
# resolve the LMS url, note we can't assume we're running in
# a same process as the LMS
exam_url_path = reverse(
'courseware.views.jump_to',
args=[exam['course_id'], exam['content_id']]
)
except NoReverseMatch:
pass
response_dict = {
'in_timed_exam': True,
'is_proctored': True,
'exam_display_name': exam['exam']['exam_name'],
'exam_url_path': '',
'taking_as_proctored': attempt['taking_as_proctored'],
'exam_display_name': exam['exam_name'],
'exam_url_path': exam_url_path,
'time_remaining_seconds': time_remaining_seconds,
'low_threshold': 30,
'critically_low_threshold': 15,
'low_threshold_sec': low_threshold,
'critically_low_threshold_sec': critically_low_threshold,
}
else:
response_dict = {
......
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