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 = { ...@@ -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): def get_attempt_status_summary(user_id, course_id, content_id):
""" """
Returns a summary about the status of the attempt for the user Returns a summary about the status of the attempt for the user
in the course_id and content_id 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: Return will be:
None: Not applicable None: Not applicable
- or - - or -
...@@ -976,6 +987,10 @@ def get_attempt_status_summary(user_id, course_id, content_id): ...@@ -976,6 +987,10 @@ def get_attempt_status_summary(user_id, course_id, content_id):
log.exception(ex) log.exception(ex)
return None 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 # let's check credit eligibility
credit_service = get_runtime_service('credit') credit_service = get_runtime_service('credit')
# practice exams always has an attempt status regardless of # practice exams always has an attempt status regardless of
......
...@@ -232,7 +232,7 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider): ...@@ -232,7 +232,7 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
# only 'Clean' and 'Rules Violation' could as passing # only 'Clean' and 'Rules Violation' could as passing
status = ( status = (
ProctoredExamStudentAttemptStatus.verified ProctoredExamStudentAttemptStatus.verified
if review_status in ['Clean', 'Suspicious'] if review_status in ['Clean', 'Rules Violation']
else ProctoredExamStudentAttemptStatus.rejected else ProctoredExamStudentAttemptStatus.rejected
) )
......
...@@ -245,8 +245,8 @@ class SoftwareSecureTests(TestCase): ...@@ -245,8 +245,8 @@ class SoftwareSecureTests(TestCase):
@ddt.data( @ddt.data(
('Clean', 'satisfied'), ('Clean', 'satisfied'),
('Suspicious', 'satisfied'), ('Rules Violation', 'satisfied'),
('Rules Violation', 'failed'), ('Suspicious', 'failed'),
('Not Reviewed', 'failed'), ('Not Reviewed', 'failed'),
) )
@ddt.unpack @ddt.unpack
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
low_threshold_sec: 0, low_threshold_sec: 0,
critically_low_threshold_sec: 0, critically_low_threshold_sec: 0,
course_id: null, course_id: null,
accessibility_time_string: '',
lastFetched: new Date() lastFetched: new Date()
}, },
getFormattedRemainingTime: function (secondsLeft) { getFormattedRemainingTime: function (secondsLeft) {
...@@ -31,19 +32,19 @@ ...@@ -31,19 +32,19 @@
}, },
getRemainingTimeState: function (secondsLeft) { getRemainingTimeState: function (secondsLeft) {
if (secondsLeft > this.get('low_threshold_sec')) { 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')) { else if (secondsLeft <= this.get('low_threshold_sec') && secondsLeft > this.get('critically_low_threshold_sec')) {
// returns the class name that has some css properties // returns the class name that has some css properties
// and it displays the user with the waring message if // and it displays the user with the waring message if
// total seconds is less than the low_threshold value. // total seconds is less than the low_threshold value.
return "low-time warning"; return "warning";
} }
else { else {
// returns the class name that has some css properties // returns the class name that has some css properties
// and it displays the user with the critical message if // and it displays the user with the critical message if
// total seconds is less than the critically_low_threshold_sec value. // total seconds is less than the critically_low_threshold_sec value.
return "low-time critical"; return "critical";
} }
} }
}); });
......
...@@ -59,7 +59,8 @@ var edx = edx || {}; ...@@ -59,7 +59,8 @@ var edx = edx || {};
}); });
$el.find('form input[type="text"]').css({ $el.find('form input[type="text"]').css({
"height": "26px", "height": "26px",
"padding": "5px 8px" "padding": "1px 8px 2px",
"font-size": "14px"
}); });
$el.find('form input[type="submit"]').css({ $el.find('form input[type="submit"]').css({
"margin-top": "10px", "margin-top": "10px",
...@@ -76,6 +77,10 @@ var edx = edx || {}; ...@@ -76,6 +77,10 @@ var edx = edx || {};
"font-size": "14px", "font-size": "14px",
"padding": "0px 10px 5px 7px" "padding": "0px 10px 5px 7px"
}); });
$el.find('form select').css({
"padding": "2px 0px 2px 2px",
"font-size": "16px"
});
}, },
getCurrentFormValues: function () { getCurrentFormValues: function () {
return { return {
......
...@@ -20,6 +20,7 @@ var edx = edx || {}; ...@@ -20,6 +20,7 @@ var edx = edx || {};
this.secondsLeft = 0; this.secondsLeft = 0;
/* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */ /* give an extra 5 seconds where the timer holds at 00:00 before page refreshes */
this.grace_period_secs = 5; this.grace_period_secs = 5;
this.first_time_rendering = true;
// we need to keep a copy here because the model will // we need to keep a copy here because the model will
// get destroyed before onbeforeunload is called // get destroyed before onbeforeunload is called
...@@ -89,6 +90,13 @@ var edx = edx || {}; ...@@ -89,6 +90,13 @@ var edx = edx || {};
var html = this.template(this.model.toJSON()); var html = this.template(this.model.toJSON());
this.$el.html(html); this.$el.html(html);
this.$el.show(); 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.updateRemainingTime(this);
this.timerId = setInterval(this.updateRemainingTime, 1000, this); this.timerId = setInterval(this.updateRemainingTime, 1000, this);
...@@ -141,11 +149,20 @@ var edx = edx || {}; ...@@ -141,11 +149,20 @@ var edx = edx || {};
} }
else { else {
self.secondsLeft = data.time_remaining_seconds; 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"); var oldState = self.$el.find('div.exam-timer').attr('class');
self.$el.find('div.exam-timer').addClass(self.model.getRemainingTimeState(self.secondsLeft)); 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)); self.$el.find('span#time_remaining_id b').html(self.model.getFormattedRemainingTime(self.secondsLeft));
if (self.secondsLeft <= -self.grace_period_secs) { if (self.secondsLeft <= -self.grace_period_secs) {
clearInterval(self.timerId); // stop the timer once the time finishes. clearInterval(self.timerId); // stop the timer once the time finishes.
......
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
<% if (is_allowances) { %> <% if (is_allowances) { %>
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content exam-allowances-content">
<table class="allowance-table"> <table class="allowance-table">
<thead> <thead>
<tr class="allowance-headings"> <tr class="allowance-headings">
......
...@@ -2,111 +2,154 @@ ...@@ -2,111 +2,154 @@
<div class="sequence proctored-exam entrance" data-exam-id="{{exam_id}}"> <div class="sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3> <h3>
{% blocktrans %} {% blocktrans %}
Would you like to take "{{ display_name }}" as a proctored exam? This exam is proctored.
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<p> <p>
{% blocktrans %} {% 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. To be eligible to earn credit for this course, you must take and pass the proctoring review for this exam.
{% endblocktrans %} {% endblocktrans %}
</p> </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> <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> <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> </a>
<p> <p>
{% blocktrans %} {% blocktrans %}
You will be guided through steps to set up online proctoring software and to perform various checks.</br> 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 %} {% endblocktrans %}
</p> </p>
<i class="fa fa-arrow-circle-right"></i> <i class="fa fa-arrow-circle-right"></i>
</button> </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> <button class="gated-sequence start-timed-exam" data-attempt-proctored=false>
<span><i class="fa fa-unlock"></i></span>
<a> <a>
{% trans "No, I want to take this exam without proctoring (and not be eligible for credit)" %} {% trans "Take this exam without proctoring." %}
</a> </a>
<i class="fa fa-arrow-circle-right"></i>
<p> <p>
{% blocktrans %} {% blocktrans %}
If you take this exam without proctoring, you will not be eligible to use this course for academic credit, I am not interested in academic credit.
even if you achieve a passing grade.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<i class="fa fa-arrow-circle-right"></i>
</button> </button>
</div> </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' %} {% include 'proctoring/seq_proctored_exam_footer.html' %}
<script type="text/javascript"> <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 inProcess = false;
var disableClickEvent = function () { var disableClickEvent = function (selector) {
inProcess = true; inProcess = true;
$('body').css('cursor', 'wait'); $('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; inProcess = false;
$('body').css('cursor', 'auto'); $('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) { if (!inProcess) {
disableClickEvent(); disableClickEvent($(this));
var action_url = $(this).data('ajax-url');
var exam_id = $(this).data('exam-id');
var attempt_proctored = $(this).data('attempt-proctored'); var attempt_proctored = $(this).data('attempt-proctored');
var start_immediately = $(this).data('start-immediately');
if (!attempt_proctored) { if (!attempt_proctored) {
var msg = gettext( enableClickEvent($(this));
"Are you sure you want to take this exam without proctoring? " + $(".proctored-exam.entrance").addClass('hidden');
"You will no longer be eligible to use this course for academic credit." $(".proctored-exam-skip-confirm-wrapper").removeClass('hidden');
); } else {
if (!confirm(msg)) { var action_url = $(this).data('ajax-url');
enableClickEvent(); var exam_id = $(this).data('exam-id');
return; 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 { } else {
return false; 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> </script>
{% load i18n %} {% load i18n %}
<div class="faq-proctoring-exam"> <div class="faq-proctoring-exam">
<h4> {% trans "See Also" %} </h4>
<p> <p>
<a class="footer-link" href="{{link_urls.faq}}" target="_blank"> <a class="footer-link" href="{{link_urls.faq}}" target="_blank">
{% blocktrans %} {% blocktrans %}
Frequently asked questions about proctoring and earning college credit About Proctored Exams
{% 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
{% endblocktrans %} {% endblocktrans %}
</a> </a>
</p> </p>
......
{% load i18n %} {% 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}}"> <div class="sequence proctored-exam instructions message-left-bar" data-exam-id="{{exam_id}}" data-exam-started-poll-url="{{exam_started_poll_url}}">
<h3>
{% blocktrans %} <div class="">
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">
<h3> <h3>
{% blocktrans %} {% blocktrans %}
Install and Set Up Proctoring Software Follow these steps to set up and start your proctored exam.
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<p> <p>
{% blocktrans %} {% 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 %} {% endblocktrans %}
</p> </p>
<p> <p>
{% blocktrans %} <span class="proctored-exam-code">{{exam_code}}</span>
2. Copy this unique exam code and paste it in the proctoring software window when you are asked for it.
{% endblocktrans %}
</p> </p>
<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>
<p> <p>
{% blocktrans %} {% 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 %} {% endblocktrans %}
</p> </p>
<p> <p>
<span><a href="{{software_download_url}}" target="_blank">Start System Check</a></span>
</p>
<p>
{% blocktrans %} {% 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 %} {% endblocktrans %}
</p> </p>
<p> <p>
{% blocktrans %} {% 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 %} {% endblocktrans %}
</p> </p>
</div> </div>
...@@ -93,31 +87,6 @@ ...@@ -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(){ $(document).ready(function(){
_waiting_for_proctored_interval = setInterval( _waiting_for_proctored_interval = setInterval(
poll_exam_started, poll_exam_started,
......
{% load i18n %} {% 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}}"> <div class="sequence proctored-exam success-left-bar instructions" data-exam-id="{{exam_id}}" data-exam-started-poll-url="{{exam_started_poll_url}}">
<h3> <div>
{% 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">
<h3> <h3>
{% blocktrans %} {% blocktrans %}
You Can Begin Your Exam Now Follow these instructions
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<p> <p>
{% blocktrans %} {% blocktrans %}
When you start your exam you will have {{total_time}} to complete it. You cannot stop the timer once you start. &#8226; Do not close the proctoring window until you have completed and submitted your exam. </br>
{% endblocktrans %} &#8226; When you start your exam you will have {{ total_time }} to complete it. </br>
{% blocktrans %} &#8226; If the allotted time expires before your end your exam, the answers you have completed up
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. to that point are submitted for grading and your proctoring session is uploaded for review. </br>
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<div> <div>
......
...@@ -2,30 +2,29 @@ ...@@ -2,30 +2,29 @@
<div class="sequence proctored-exam entrance" data-exam-id="{{exam_id}}"> <div class="sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3> <h3>
{% blocktrans %} {% blocktrans %}
Would you like to take "{{ display_name }}" as a practice proctored exam? Try a proctored exam
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<p> <p>
{% blocktrans %} {% 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 %} {% endblocktrans %}
</p> </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> <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> <a>
{% trans "Yes, I want to take this practice exam with online proctoring" %} {% trans "Continue to my practice exam" %}
</a> </a>
<p> <p>
{% blocktrans %} {% blocktrans %}
You will be guided through steps to set up online proctoring software and to perform various checks. 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 %} {% endblocktrans %}
</p> </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> </button>
</div> </div>
{% include 'proctoring/seq_proctored_exam_footer.html' %} {% include 'proctoring/seq_proctored_practice_exam_footer.html' %}
<script type="text/javascript"> <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 @@ ...@@ -2,7 +2,7 @@
# pylint: disable=too-many-lines, invalid-name # pylint: disable=too-many-lines, invalid-name
""" """
All tests for the models.py All tests for the api.py
""" """
import ddt import ddt
from datetime import datetime, timedelta from datetime import datetime, timedelta
...@@ -95,7 +95,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -95,7 +95,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.disabled_exam_id = self._create_disabled_exam() self.disabled_exam_id = self._create_disabled_exam()
# Messages for get_student_view # 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.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_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' self.exam_time_error_msg = 'There was a problem with your proctoring session'
...@@ -105,9 +105,9 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -105,9 +105,9 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.proctored_exam_verified_msg = 'Your proctoring session was reviewed and passed all requirements' 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.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.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.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.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_subject = 'Proctoring Session Results Update'
self.proctored_exam_email_body = 'the status of your proctoring session review' self.proctored_exam_email_body = 'the status of your proctoring session review'
...@@ -148,7 +148,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -148,7 +148,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam_name=self.exam_name, exam_name=self.exam_name,
time_limit_mins=self.default_time_limit, time_limit_mins=self.default_time_limit,
is_practice_exam=True, is_practice_exam=True,
is_proctored=False is_proctored=True
) )
def _create_disabled_exam(self): def _create_disabled_exam(self):
...@@ -645,7 +645,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -645,7 +645,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
} }
) )
self.assertIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response) 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 # try practice exam variant
rendered_response = get_student_view( rendered_response = get_student_view(
...@@ -659,7 +659,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -659,7 +659,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
'is_practice_exam': True, '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 def test_get_honor_view_with_practice_exam(self): # pylint: disable=invalid-name
""" """
...@@ -794,25 +794,6 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -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): def test_declined_attempt(self):
""" """
Make sure that a declined attempt does not show proctoring Make sure that a declined attempt does not show proctoring
...@@ -1073,7 +1054,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1073,7 +1054,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
) )
self.assertNotIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response) self.assertNotIn('data-exam-id="%d"' % self.proctored_exam_id, rendered_response)
self.assertIn(self.timed_exam_msg % self.exam_name, 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 def test_get_studentview_completed_timed_exam(self): # pylint: disable=invalid-name
""" """
...@@ -1462,6 +1443,30 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1462,6 +1443,30 @@ class ProctoredExamApiTests(LoggedInTestCase):
@ddt.data( @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, { ProctoredExamStudentAttemptStatus.eligible, {
'status': ProctoredExamStudentAttemptStatus.eligible, 'status': ProctoredExamStudentAttemptStatus.eligible,
'short_description': 'Ungraded Practice Exam', 'short_description': 'Ungraded Practice Exam',
......
...@@ -502,6 +502,19 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -502,6 +502,19 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertEqual(response_data['proctored_exam']['id'], proctored_exam.id) self.assertEqual(response_data['proctored_exam']['id'], proctored_exam.id)
self.assertIsNotNone(response_data['started_at']) self.assertIsNotNone(response_data['started_at'])
self.assertIsNone(response_data['completed_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): def test_attempt_ready_to_start(self):
""" """
...@@ -1128,6 +1141,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase): ...@@ -1128,6 +1141,8 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
self.assertEqual(data['exam_display_name'], 'Test Exam') self.assertEqual(data['exam_display_name'], 'Test Exam')
self.assertEqual(data['low_threshold_sec'], 1080) self.assertEqual(data['low_threshold_sec'], 1080)
self.assertEqual(data['critically_low_threshold_sec'], 270) 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): def test_get_expired_attempt(self):
""" """
......
...@@ -6,6 +6,7 @@ import logging ...@@ -6,6 +6,7 @@ import logging
import pytz import pytz
from datetime import datetime from datetime import datetime
from django.utils.translation import ugettext as _
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
...@@ -40,7 +41,7 @@ from edx_proctoring.exceptions import ( ...@@ -40,7 +41,7 @@ from edx_proctoring.exceptions import (
from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAttemptSerializer from edx_proctoring.serializers import ProctoredExamSerializer, ProctoredExamStudentAttemptSerializer
from edx_proctoring.models import ProctoredExamStudentAttemptStatus, ProctoredExamStudentAttempt 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 ATTEMPTS_PER_PAGE = 25
...@@ -296,6 +297,16 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView): ...@@ -296,6 +297,16 @@ class StudentProctoredExamAttempt(AuthenticatedAPIView):
attempt['time_remaining_seconds'] = time_remaining_seconds 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( return Response(
data=attempt, data=attempt,
status=status.HTTP_200_OK status=status.HTTP_200_OK
...@@ -481,6 +492,7 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView): ...@@ -481,6 +492,7 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
response_dict = { response_dict = {
'in_timed_exam': True, 'in_timed_exam': True,
'taking_as_proctored': attempt['taking_as_proctored'], 'taking_as_proctored': attempt['taking_as_proctored'],
'exam_type': _('proctored') if attempt['taking_as_proctored'] else _('timed'),
'exam_display_name': exam['exam_name'], 'exam_display_name': exam['exam_name'],
'exam_url_path': exam_url_path, 'exam_url_path': exam_url_path,
'time_remaining_seconds': time_remaining_seconds, 'time_remaining_seconds': time_remaining_seconds,
...@@ -488,6 +500,9 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView): ...@@ -488,6 +500,9 @@ class StudentProctoredExamAttemptCollection(AuthenticatedAPIView):
'critically_low_threshold_sec': critically_low_threshold, 'critically_low_threshold_sec': critically_low_threshold,
'course_id': exam['course_id'], 'course_id': exam['course_id'],
'attempt_id': attempt['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'] 'attempt_status': attempt['status']
} }
else: else:
......
...@@ -34,7 +34,7 @@ def load_requirements(*requirements_paths): ...@@ -34,7 +34,7 @@ def load_requirements(*requirements_paths):
setup( setup(
name='edx-proctoring', name='edx-proctoring',
version='0.8.3', version='0.9.6b',
description='Proctoring subsystem for Open edX', description='Proctoring subsystem for Open edX',
long_description=open('README.md').read(), long_description=open('README.md').read(),
author='edX', author='edX',
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
logilab-common==0.63.2 logilab-common==0.63.2
coverage==3.7.1 coverage==3.7.1
astroid==1.3.4 astroid==1.3.4
django_nose django_nose==1.4.1
nose==1.3.3 nose==1.3.3
httpretty==0.8.0 httpretty==0.8.0
pep8==1.6.2 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