Commit da5746f5 by chrisndodge

Merge pull request #145 from edx/rc/2015-09-15

Rc/2015 09 15
parents 50aa9505 da9b5c2a
......@@ -952,12 +952,23 @@ PRACTICE_STATUS_SUMMARY_MAP = {
}
}
TIMED_EXAM_STATUS_SUMMARY_MAP = {
'_default': {
'short_description': _('Timed Exam'),
'suggested_icon': 'fa-clock-o',
'in_completed_state': False
}
}
def get_attempt_status_summary(user_id, course_id, content_id):
"""
Returns a summary about the status of the attempt for the user
in the course_id and content_id
If the exam is timed exam only then we simply
return the dictionary with timed exam default summary
Return will be:
None: Not applicable
- or -
......@@ -976,6 +987,10 @@ def get_attempt_status_summary(user_id, course_id, content_id):
log.exception(ex)
return None
# check if the exam is not proctored
if not exam['is_proctored']:
return TIMED_EXAM_STATUS_SUMMARY_MAP['_default']
# let's check credit eligibility
credit_service = get_runtime_service('credit')
# practice exams always has an attempt status regardless of
......
......@@ -232,7 +232,7 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
# only 'Clean' and 'Rules Violation' could as passing
status = (
ProctoredExamStudentAttemptStatus.verified
if review_status in ['Clean', 'Suspicious']
if review_status in ['Clean', 'Rules Violation']
else ProctoredExamStudentAttemptStatus.rejected
)
......
......@@ -245,8 +245,8 @@ class SoftwareSecureTests(TestCase):
@ddt.data(
('Clean', 'satisfied'),
('Suspicious', 'satisfied'),
('Rules Violation', 'failed'),
('Rules Violation', 'satisfied'),
('Suspicious', 'failed'),
('Not Reviewed', 'failed'),
)
@ddt.unpack
......
......@@ -14,6 +14,7 @@
low_threshold_sec: 0,
critically_low_threshold_sec: 0,
course_id: null,
accessibility_time_string: '',
lastFetched: new Date()
},
getFormattedRemainingTime: function (secondsLeft) {
......@@ -31,19 +32,19 @@
},
getRemainingTimeState: function (secondsLeft) {
if (secondsLeft > this.get('low_threshold_sec')) {
return "";
return null;
}
else if (secondsLeft <= this.get('low_threshold_sec') && secondsLeft > this.get('critically_low_threshold_sec')) {
// returns the class name that has some css properties
// and it displays the user with the waring message if
// total seconds is less than the low_threshold value.
return "low-time warning";
return "warning";
}
else {
// returns the class name that has some css properties
// and it displays the user with the critical message if
// total seconds is less than the critically_low_threshold_sec value.
return "low-time critical";
return "critical";
}
}
});
......
......@@ -59,7 +59,8 @@ var edx = edx || {};
});
$el.find('form input[type="text"]').css({
"height": "26px",
"padding": "5px 8px"
"padding": "1px 8px 2px",
"font-size": "14px"
});
$el.find('form input[type="submit"]').css({
"margin-top": "10px",
......@@ -76,6 +77,10 @@ var edx = edx || {};
"font-size": "14px",
"padding": "0px 10px 5px 7px"
});
$el.find('form select').css({
"padding": "2px 0px 2px 2px",
"font-size": "16px"
});
},
getCurrentFormValues: function () {
return {
......
......@@ -20,6 +20,7 @@ var edx = edx || {};
this.secondsLeft = 0;
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this.grace_period_secs = 5;
this.first_time_rendering = true;
// we need to keep a copy here because the model will
// get destroyed before onbeforeunload is called
......@@ -89,6 +90,13 @@ var edx = edx || {};
var html = this.template(this.model.toJSON());
this.$el.html(html);
this.$el.show();
// only render the accesibility string the first time we render after
// page load (then we will update on time left warnings)
if (this.first_time_rendering) {
this.accessibility_time_string = this.model.get('accessibility_time_string');
this.$el.find('.timer-announce').html(this.accessibility_time_string);
this.first_time_rendering = false;
}
this.updateRemainingTime(this);
this.timerId = setInterval(this.updateRemainingTime, 1000, this);
......@@ -141,11 +149,20 @@ var edx = edx || {};
}
else {
self.secondsLeft = data.time_remaining_seconds;
self.accessibility_time_string = data.accessibility_time_string;
}
});
}
self.$el.find('div.exam-timer').removeClass("low-time warning critical");
self.$el.find('div.exam-timer').addClass(self.model.getRemainingTimeState(self.secondsLeft));
var oldState = self.$el.find('div.exam-timer').attr('class');
var newState = self.model.getRemainingTimeState(self.secondsLeft);
if (newState !== null && !self.$el.find('div.exam-timer').hasClass(newState)) {
self.$el.find('div.exam-timer').removeClass("warning critical");
self.$el.find('div.exam-timer').addClass("low-time " + newState);
// refresh accessibility string
self.$el.find('.timer-announce').html(self.accessibility_time_string);
}
self.$el.find('span#time_remaining_id b').html(self.model.getFormattedRemainingTime(self.secondsLeft));
if (self.secondsLeft <= -self.grace_period_secs) {
clearInterval(self.timerId); // stop the timer once the time finishes.
......
......@@ -7,8 +7,7 @@
<% if (is_allowances) { %>
<div class="wrapper-content wrapper">
<section class="content">
<section class="content exam-allowances-content">
<table class="allowance-table">
<thead>
<tr class="allowance-headings">
......
......@@ -2,111 +2,154 @@
<div class="sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
Would you like to take "{{ display_name }}" as a proctored exam?
This exam is proctored.
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
As a verified learner in this course, you have the option of taking this exam with online proctoring.
To be eligible to earn credit for this course, you must take and pass the proctoring review for this exam.
{% endblocktrans %}
</p>
<button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false>
<span><i class="fa fa-lock"></i></span>
<a>
{% trans "Yes, I want to take this exam with online proctoring (and be eligible for credit)" %}
{% trans "Continue to my proctored exam. I want to be eligible for credit." %}
</a>
<p>
{% blocktrans %}
You will be guided through steps to set up online proctoring software and to perform various checks.</br>
&#8226; Have your photo ID ready so that you can verify your identity. </br>
&#8226; Be ready to start your exam after you complete the proctoring setup. </br>
{% endblocktrans %}
</p>
<i class="fa fa-arrow-circle-right"></i>
</button>
<button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=false data-start-immediately=true>
<span><i class="fa fa-unlock"></i></span>
<button class="gated-sequence start-timed-exam" data-attempt-proctored=false>
<a>
{% trans "No, I want to take this exam without proctoring (and not be eligible for credit)" %}
{% trans "Take this exam without proctoring." %}
</a>
<i class="fa fa-arrow-circle-right"></i>
<p>
{% blocktrans %}
If you take this exam without proctoring, you will not be eligible to use this course for academic credit,
even if you achieve a passing grade.
I am not interested in academic credit.
{% endblocktrans %}
</p>
<i class="fa fa-arrow-circle-right"></i>
</button>
</div>
<div class="proctored-exam-skip-confirm-wrapper hidden">
<div class="proctored-exam-skip-confirm">
<div class="msg-title">
{% blocktrans %}
Are you sure you want to take this exam without proctoring?
{% endblocktrans %}
</div>
<div class="msg-content">
{% blocktrans %}
If you take this exam without proctoring, you will <strong> no longer be eligible for academic credit. </strong>
{% endblocktrans %}
</div>
<div class="proctored-exam-skip-actions">
<button class="proctored-exam-skip-confirm-button btn btn-primary btn-base" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}">
{% trans "Continue Exam Without Proctoring" %}
</button>
<button class="proctored-exam-skip-cancel-button btn btn-default btn-base">
{% trans "Go Back" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
<script type="text/javascript">
var startProctoredExam = function(selector, exam_id, action_url, start_immediately, attempt_proctored) {
$.post(
action_url,
{
"exam_id": exam_id,
"attempt_proctored": attempt_proctored,
"start_clock": start_immediately
},
function(data) {
// reload the page, because we've unlocked it
location.reload();
}
).fail(function(){
enableClickEvent(selector);
var msg = gettext(
"There has been a problem starting your exam.\n\n" +
"Possible reasons are that your account has not been fully activated,\n" +
"you have are experiencing a network connection problem, or there has been\n" +
"a service disruption. Please check these and try again."
);
alert(msg);
});
};
var inProcess = false;
var disableClickEvent = function () {
var disableClickEvent = function (selector) {
inProcess = true;
$('body').css('cursor', 'wait');
$('.start-timed-exam').css('cursor', 'wait');
selector.each(function() {
$( this ).css('cursor', 'wait');
});
};
var enableClickEvent = function () {
var enableClickEvent = function (selector) {
inProcess = false;
$('body').css('cursor', 'auto');
$('.start-timed-exam').css('cursor', 'auto');
selector.each(function() {
$( this ).css('cursor', 'pointer');
});
};
$('.start-timed-exam').click(function () {
$('.start-timed-exam').click(
function() {
if (!inProcess) {
disableClickEvent();
var action_url = $(this).data('ajax-url');
var exam_id = $(this).data('exam-id');
disableClickEvent($(this));
var attempt_proctored = $(this).data('attempt-proctored');
var start_immediately = $(this).data('start-immediately');
if (!attempt_proctored) {
var msg = gettext(
"Are you sure you want to take this exam without proctoring? " +
"You will no longer be eligible to use this course for academic credit."
);
if (!confirm(msg)) {
enableClickEvent();
return;
}
enableClickEvent($(this));
$(".proctored-exam.entrance").addClass('hidden');
$(".proctored-exam-skip-confirm-wrapper").removeClass('hidden');
} else {
var action_url = $(this).data('ajax-url');
var exam_id = $(this).data('exam-id');
var start_immediately = $(this).data('start-immediately');
startProctoredExam($(this), exam_id, action_url, start_immediately, attempt_proctored);
}
if (typeof action_url === "undefined") {
enableClickEvent();
return false;
}
var self = $(this);
$.post(
action_url,
{
"exam_id": exam_id,
"attempt_proctored": attempt_proctored,
"start_clock": start_immediately
},
function (data) {
// reload the page, because we've unlocked it
location.reload();
}
).fail(function(){
enableClickEvent();
var msg = gettext(
"There has been a problem starting your exam.\n\n" +
"Possible reasons are that your account has not been fully activated,\n" +
"you have are experiencing a network connection problem, or there has been\n" +
"a service disruption. Please check these and try again."
);
alert(msg);
});
} else {
return false;
}
}
);
$('.proctored-exam-skip-confirm-button').click(
function(event){
if (!inProcess) {
// find the all the buttons and call disableClickEvent
// on the events
var events = $(this).parent().find('button');
disableClickEvent(events);
var action_url = $(this).data('ajax-url');
var exam_id = $(this).data('exam-id');
startProctoredExam(events, exam_id, action_url, true, false);
} else {
return false
}
}
);
$('.proctored-exam-skip-cancel-button').click(
function(event){
if (!inProcess) {
enableClickEvent($(this));
$(".proctored-exam.entrance").removeClass('hidden');
$(".proctored-exam-skip-confirm-wrapper").addClass('hidden');
} else {
return false;
}
}
)
</script>
{% load i18n %}
<div class="faq-proctoring-exam">
<h4> {% trans "See Also" %} </h4>
<p>
<a class="footer-link" href="{{link_urls.faq}}" target="_blank">
{% blocktrans %}
Frequently asked questions about proctoring and earning college credit
{% endblocktrans %}
</a>
<a class="footer-link" href="{{link_urls.online_proctoring_rules}}" target="_blank">
{% blocktrans %}
Online proctoring rules
{% endblocktrans %}
</a>
<a class="footer-link" href="{{link_urls.tech_requirements}}" target="_blank">
{% blocktrans %}
Technical Requirements for taking a proctored exam
About Proctored Exams
{% endblocktrans %}
</a>
</p>
......
{% load i18n %}
<div class="sequence proctored-exam instructions message-top-bar" data-exam-id="{{exam_id}}" data-exam-started-poll-url="{{exam_started_poll_url}}">
<h3>
{% blocktrans %}
You Have Chosen To Take a Proctored Exam
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
You must set up and start the proctoring software before you begin your exam
{% endblocktrans %}
</p>
<div class="proctored-exam-message">
<div class="sequence proctored-exam instructions message-left-bar" data-exam-id="{{exam_id}}" data-exam-started-poll-url="{{exam_started_poll_url}}">
<div class="">
<h3>
{% blocktrans %}
Install and Set Up Proctoring Software
Follow these steps to set up and start your proctored exam.
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
1. Open the proctoring software installation window and follow the instructions to <a href="{{software_download_url}}" target="_blank">install and set up the proctoring software</a>.
1. Copy this unique exam code. You will be prompted to paste this code later before you start the exam.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
2. Copy this unique exam code and paste it in the proctoring software window when you are asked for it.
{% endblocktrans %}
<span class="proctored-exam-code">{{exam_code}}</span>
</p>
<p>
<span class="proctored-exam-code" role="application">{{exam_code}}</span>&nbsp;<button class='proctored-exam-select-code'>Select exam code</button>
{% blocktrans %}
Select the exam code, then copy it using Command+C (Mac) or Control+C (Windows).
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
Do not share this code. It is linked to your {{platform_name}} account and can be used only once.
2. Follow the link below to set up proctoring.
{% endblocktrans %}
</p>
<p>
<span><a href="{{software_download_url}}" target="_blank">Start System Check</a></span>
</p>
<p>
{% blocktrans %}
3. Keep the proctoring session window open while you are taking the exam. If you close it, the proctoring recording is stopped and you will not successfully complete the proctored exam.
A new window will open. You will run a system check before downloading the proctoring application.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
4. Return to the {{platform_name}} courseware to start your exam.
You will be asked to verify your identity before you begin the exam. Make sure you have valid photo identification, such as a driver's license or passport, before you continue.
{% endblocktrans %}
</p>
</div>
......@@ -93,31 +87,6 @@
}
);
$('.proctored-exam-select-code').click(function() {
selectText($('.proctored-exam-code'));
$(event.currentTarget).blur();
});
function selectText(textToBeSelected) {
// this selects all the text in an element
// http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div
var doc = document,
range, selection,
element = textToBeSelected[0];
if (doc.body.createTextRange) {
range = doc.body.createTextRange();
range.moveToElementText(element);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = doc.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
}
}
$(document).ready(function(){
_waiting_for_proctored_interval = setInterval(
poll_exam_started,
......
{% load i18n %}
<div class="sequence proctored-exam success-top-bar instructions" data-exam-id="{{exam_id}}" data-exam-started-poll-url="{{exam_started_poll_url}}">
<h3>
{% blocktrans %}
Your Proctoring Installation and Set Up is Complete
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
Do not close the proctoring session window until you have completed and submitted your exam.
{% endblocktrans %}
</p>
<div class="proctored-exam-message">
<div class="sequence proctored-exam success-left-bar instructions" data-exam-id="{{exam_id}}" data-exam-started-poll-url="{{exam_started_poll_url}}">
<div>
<h3>
{% blocktrans %}
You Can Begin Your Exam Now
Follow these instructions
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
When you start your exam you will have {{total_time}} to complete it. You cannot stop the timer once you start.
{% endblocktrans %}
{% blocktrans %}
If the allotted time expires before you mark your exam as ended, the answers you have completed up to that point are submitted for grading, and your proctoring session is uploaded for review.
&#8226; Do not close the proctoring window until you have completed and submitted your exam. </br>
&#8226; When you start your exam you will have {{ total_time }} to complete it. </br>
&#8226; If the allotted time expires before your end your exam, the answers you have completed up
to that point are submitted for grading and your proctoring session is uploaded for review. </br>
{% endblocktrans %}
</p>
<div>
......
......@@ -2,30 +2,29 @@
<div class="sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
Would you like to take "{{ display_name }}" as a practice proctored exam?
Try a proctored exam
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
You can try this practice exam as many times as you want.
Get familiar with proctoring for real exams later in the course. This practice exam has no impact
on your grade in the course.
{% endblocktrans %}
</p>
<button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false>
<span><i class="fa fa-lock"></i></span>
<a>
{% trans "Yes, I want to take this practice exam with online proctoring" %}
{% trans "Continue to my practice exam" %}
</a>
<p>
{% blocktrans %}
You will be guided through steps to set up online proctoring software and to perform various checks.
When you start the exam, the timer on the right shows the time remaining in the exam.
{% endblocktrans %}
</p>
<i class="fa fa-arrow-circle-right start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false></i>
<i class="fa fa-arrow-circle-right practice-exam start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false></i>
</button>
</div>
{% include 'proctoring/seq_proctored_exam_footer.html' %}
{% include 'proctoring/seq_proctored_practice_exam_footer.html' %}
<script type="text/javascript">
......
{% load i18n %}
<div class="faq-proctoring-exam">
<p>
<a class="footer-link" href="{{link_urls.faq}}" target="_blank">
{% blocktrans %}
About Proctored Exams
{% endblocktrans %}
</a>
</p>
</div>
......@@ -2,7 +2,7 @@
# pylint: disable=too-many-lines, invalid-name
"""
All tests for the models.py
All tests for the api.py
"""
import ddt
from datetime import datetime, timedelta
......@@ -95,7 +95,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.disabled_exam_id = self._create_disabled_exam()
# Messages for get_student_view
self.start_an_exam_msg = 'Would you like to take "%s" as a proctored exam?'
self.start_an_exam_msg = 'This exam is proctored.'
self.timed_exam_msg = '%s is a Timed Exam'
self.exam_time_expired_msg = 'You did not complete the exam in the allotted time'
self.exam_time_error_msg = 'There was a problem with your proctoring session'
......@@ -105,9 +105,9 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.proctored_exam_verified_msg = 'Your proctoring session was reviewed and passed all requirements'
self.proctored_exam_rejected_msg = 'Your proctoring session was reviewed and did not pass requirements'
self.timed_exam_completed_msg = 'This is the end of your timed exam'
self.start_a_practice_exam_msg = 'Would you like to take "%s" as a practice proctored exam?'
self.start_a_practice_exam_msg = 'Get familiar with proctoring for real exams later in the course.'
self.practice_exam_submitted_msg = 'You have submitted this practice proctored exam'
self.ready_to_start_msg = 'Your Proctoring Installation and Set Up is Complete'
self.ready_to_start_msg = 'Follow these instructions'
self.practice_exam_failed_msg = 'There was a problem with your practice proctoring session'
self.proctored_exam_email_subject = 'Proctoring Session Results Update'
self.proctored_exam_email_body = 'the status of your proctoring session review'
......@@ -148,7 +148,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_name=self.exam_name,
time_limit_mins=self.default_time_limit,
is_practice_exam=True,
is_proctored=False
is_proctored=True
)
def _create_disabled_exam(self):
......@@ -645,7 +645,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
}
)
self.assertIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response)
self.assertIn(self.start_an_exam_msg % self.exam_name, rendered_response)
self.assertIn(self.start_an_exam_msg, rendered_response)
# try practice exam variant
rendered_response = get_student_view(
......@@ -659,7 +659,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
'is_practice_exam': True,
}
)
self.assertIn(self.start_a_practice_exam_msg % self.exam_name, rendered_response)
self.assertIn(self.start_a_practice_exam_msg, rendered_response)
def test_get_honor_view_with_practice_exam(self): # pylint: disable=invalid-name
"""
......@@ -794,25 +794,6 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
)
def test_get_studentview_unstarted_exam(self): # pylint: disable=invalid-name
"""
Test for get_student_view proctored exam which has not started yet.
"""
self._create_unstarted_exam_attempt()
rendered_response = get_student_view(
user_id=self.user_id,
course_id=self.course_id,
content_id=self.content_id,
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
)
self.assertIn(self.chose_proctored_exam_msg, rendered_response)
def test_declined_attempt(self):
"""
Make sure that a declined attempt does not show proctoring
......@@ -1073,7 +1054,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
self.assertNotIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response)
self.assertIn(self.timed_exam_msg % self.exam_name, rendered_response)
self.assertNotIn(self.start_an_exam_msg % self.exam_name, rendered_response)
self.assertNotIn(self.start_an_exam_msg, rendered_response)
def test_get_studentview_completed_timed_exam(self): # pylint: disable=invalid-name
"""
......@@ -1462,6 +1443,30 @@ class ProctoredExamApiTests(LoggedInTestCase):
@ddt.data(
(
{
'short_description': 'Timed Exam',
'suggested_icon': 'fa-clock-o',
'in_completed_state': False
},
)
)
@ddt.unpack
def test_timed_exam_status_summary(self, expected):
"""
Assert that we get the expected status summaries
for the timed exams.
"""
timed_exam = get_exam_by_id(self.timed_exam)
summary = get_attempt_status_summary(
self.user.id,
timed_exam['course_id'],
timed_exam['content_id']
)
self.assertIn(summary, [expected])
@ddt.data(
(
ProctoredExamStudentAttemptStatus.eligible, {
'status': ProctoredExamStudentAttemptStatus.eligible,
'short_description': 'Ungraded Practice Exam',
......
......@@ -502,6 +502,19 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertEqual(response_data['proctored_exam']['id'], proctored_exam.id)
self.assertIsNotNone(response_data['started_at'])
self.assertIsNone(response_data['completed_at'])
# make sure we have the accessible human string
self.assertEqual(response_data['accessibility_time_string'], 'you have 1 hour and 30 minutes remaining')
# check the special casing of the human string when under a minute
reset_time = datetime.now(pytz.UTC) + timedelta(minutes=90, seconds=30)
with freeze_time(reset_time):
response = self.client.get(
reverse('edx_proctoring.proctored_exam.attempt', args=[attempt_id])
)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response_data['accessibility_time_string'], 'you have less than a minute remaining')
def test_attempt_ready_to_start(self):
"""
......@@ -1128,6 +1141,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertEqual(data['exam_display_name'], 'Test Exam')
self.assertEqual(data['low_threshold_sec'], 1080)
self.assertEqual(data['critically_low_threshold_sec'], 270)
# make sure we have the accessible human string
self.assertEqual(data['accessibility_time_string'], 'you have 1 hour and 30 minutes remaining')
def test_get_expired_attempt(self):
"""
......
......@@ -6,6 +6,7 @@ import logging
import pytz
from datetime import datetime
from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator
from django.conf import settings
from django.core.urlresolvers import reverse, NoReverseMatch
......@@ -40,7 +41,7 @@ from edx_proctoring.exceptions import (
from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAttemptSerializer
from edx_proctoring.models import ProctoredExamStudentAttemptStatus, ProctoredExamStudentAttempt
from .utils import AuthenticatedAPIView, get_time_remaining_for_attempt
from .utils import AuthenticatedAPIView, get_time_remaining_for_attempt, humanized_time
ATTEMPTS_PER_PAGE = 25
......@@ -296,6 +297,16 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
attempt['time_remaining_seconds'] = time_remaining_seconds
accessibility_time_string = _('you have {remaining_time} remaining').format(
remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0))))
# special case if we are less than a minute, since we don't produce
# text translations of granularity at the seconds range
if time_remaining_seconds < 60:
accessibility_time_string = _('you have less than a minute remaining')
attempt['accessibility_time_string'] = accessibility_time_string
return Response(
data=attempt,
status=status.HTTP_200_OK
......@@ -481,6 +492,7 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
response_dict = {
'in_timed_exam': True,
'taking_as_proctored': attempt['taking_as_proctored'],
'exam_type': _('proctored') if attempt['taking_as_proctored'] else _('timed'),
'exam_display_name': exam['exam_name'],
'exam_url_path': exam_url_path,
'time_remaining_seconds': time_remaining_seconds,
......@@ -488,6 +500,9 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
'critically_low_threshold_sec': critically_low_threshold,
'course_id': exam['course_id'],
'attempt_id': attempt['id'],
'accessibility_time_string': _('you have {remaining_time} remaining').format(
remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
),
'attempt_status': attempt['status']
}
else:
......
......@@ -34,7 +34,7 @@ def load_requirements(*requirements_paths):
setup(
name='edx-proctoring',
version='0.8.3',
version='0.9.6b',
description='Proctoring subsystem for Open edX',
long_description=open('README.md').read(),
author='edX',
......
......@@ -2,7 +2,7 @@
logilab-common==0.63.2
coverage==3.7.1
astroid==1.3.4
django_nose
django_nose==1.4.1
nose==1.3.3
httpretty==0.8.0
pep8==1.6.2
......
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