Commit 1b9591e5 by Chris Dodge

Add notion of prerequisites

parent 8a1a8152
{% load i18n %}
<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>
<script type="text/javascript">
$('.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');
// this is defined in parent template
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>
......@@ -34,122 +34,6 @@
</p>
</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 'proctored_exam/confirm-decline.html' %}
{% include '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 (selector) {
inProcess = true;
$('body').css('cursor', 'wait');
selector.each(function() {
$( this ).css('cursor', 'wait');
});
};
var enableClickEvent = function (selector) {
inProcess = false;
$('body').css('cursor', 'auto');
selector.each(function() {
$( this ).css('cursor', 'pointer');
});
};
$('.start-timed-exam').click(
function() {
if (!inProcess) {
disableClickEvent($(this));
var attempt_proctored = $(this).data('attempt-proctored');
if (!attempt_proctored) {
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);
}
} 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="failure sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
This exam is proctored.
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
You have not completed the prerequisites for this exam. All requirements must be satisfied before you can take this proctored exam and be eligible for credit. See your <a href="{{progress_page_url}}">Progress</a> page for a list of requirements in the order that they must be completed.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
You did not satisfy the following prerequisites:
{% endblocktrans %}
<ol style='list-style-type: disc'>
{% for requirement in prerequisite_status.failed_prerequisites %}
<li>
{% if requirement.jumpto_url %}
<a href='{{requirement.jumpto_url}}'>{{requirement.display_name}}</a>
{% else %}
{{requirement.display_name}}
{% endif %}
</li>
{% endfor %}
</ol>
</p>
<p>
{% blocktrans %}
Due to unsatisfied prerequisites, you can only take this exam without proctoring.
{% endblocktrans %}
</p>
<button class="gated-sequence start-timed-exam" data-attempt-proctored=false>
<a>
{% trans "Take this exam without proctoring." %}
</a>
<i class="fa fa-arrow-circle-right"></i>
<p>
{% blocktrans %}
I am not interested in academic credit.
{% endblocktrans %}
</p>
</button>
</div>
{% include 'proctored_exam/confirm-decline.html' %}
{% include 'proctored_exam/footer.html' %}
......@@ -8,3 +8,72 @@
</a>
</p>
</div>
<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. Check your account or network connection and try again."
);
alert(msg);
});
};
var inProcess = false;
var disableClickEvent = function (selector) {
inProcess = true;
$('body').css('cursor', 'wait');
selector.each(function() {
$( this ).css('cursor', 'wait');
});
};
var enableClickEvent = function (selector) {
inProcess = false;
$('body').css('cursor', 'auto');
selector.each(function() {
$( this ).css('cursor', 'pointer');
});
};
$('.start-timed-exam').click(
function() {
if (!inProcess) {
disableClickEvent($(this));
var attempt_proctored = $(this).data('attempt-proctored');
if (!attempt_proctored) {
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);
}
} else {
return false;
}
}
);
</script>
{% load i18n %}
<div class="warning sequence proctored-exam entrance" data-exam-id="{{exam_id}}">
<h3>
{% blocktrans %}
This exam is proctored.
{% endblocktrans %}
</h3>
<p>
{% blocktrans %}
You have not completed the prerequisites for this exam. All requirements must be satisfied before you can take this proctored exam and be eligible for credit. See your <a href="{{progress_page_url}}">Progress</a> page for a list of requirements in the order that they must be completed.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
The following prerequisites are in a <strong>pending</strong> state and must be successfully completed before you can proceed:
{% endblocktrans %}
<ol style='list-style-type: disc'>
{% for requirement in prerequisite_status.pending_prerequisites %}
<li>
{% if requirement.jumpto_url %}
<a href='{{requirement.jumpto_url}}'>{{requirement.display_name}}</a>
{% else %}
{{requirement.display_name}}
{% endif %}
</li>
{% endfor %}
</ol>
</p>
<p>
{% blocktrans %}
To take this exam with proctoring, confirm that you have successfully completed these requirements. You can also take this exam without proctoring, but you will not be eligible for credit.
{% endblocktrans %}
</p>
<button class="gated-sequence start-timed-exam" data-attempt-proctored=false>
<a>
{% trans "Take this exam without proctoring." %}
</a>
<i class="fa fa-arrow-circle-right"></i>
<p>
{% blocktrans %}
I am not interested in academic credit.
{% endblocktrans %}
</p>
</button>
</div>
{% include 'proctored_exam/confirm-decline.html' %}
{% include 'proctored_exam/footer.html' %}
......@@ -37,7 +37,9 @@ from edx_proctoring.api import (
update_attempt_status,
get_attempt_status_summary,
update_exam_attempt,
_check_for_attempt_timeout
_check_for_attempt_timeout,
_get_ordered_prerequisites,
_are_prerequirements_satisfied,
)
from edx_proctoring.exceptions import (
ProctoredExamAlreadyExists,
......@@ -119,6 +121,39 @@ class ProctoredExamApiTests(LoggedInTestCase):
set_runtime_service('credit', MockCreditService())
set_runtime_service('instructor', MockInstructorService(is_user_course_staff=True))
self.prerequisites = [
{
'namespace': 'proctoring',
'name': 'proc1',
'order': 2,
'status': 'satisfied',
},
{
'namespace': 'reverification',
'name': 'rever1',
'order': 1,
'status': 'satisfied',
},
{
'namespace': 'grade',
'name': 'grade1',
'order': 0,
'status': 'pending',
},
{
'namespace': 'reverification',
'name': 'rever2',
'order': 3,
'status': 'failed',
},
{
'namespace': 'proctoring',
'name': 'proc2',
'order': 4,
'status': 'pending',
},
]
def _create_proctored_exam(self):
"""
Calls the api's create_exam to create an exam object.
......@@ -756,14 +791,18 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.assertIsNone(rendered_response)
@ddt.data(
('reverification', None, True, True, False),
('reverification', 'failed', False, False, True),
('reverification', 'satisfied', True, True, False),
('grade', 'failed', True, False, False)
('reverification', None, 'The following prerequisites are in a <strong>pending</strong> state', True),
('reverification', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True),
('reverification', 'failed', 'You did not satisfy the following prerequisites', True),
('reverification', 'satisfied', 'To be eligible to earn credit for this course', False),
('proctored_exam', None, 'The following prerequisites are in a <strong>pending</strong> state', True),
('proctored_exam', 'pending', 'The following prerequisites are in a <strong>pending</strong> state', True),
('proctored_exam', 'failed', 'You did not satisfy the following prerequisites', True),
('proctored_exam', 'satisfied', 'To be eligible to earn credit for this course', False),
('grade', 'failed', 'To be eligible to earn credit for this course', False)
)
@ddt.unpack
def test_prereq_scenarios(self, namespace, req_status, show_proctored,
pre_create_attempt, mark_as_declined):
def test_prereq_scenarios(self, namespace, req_status, expected_content, should_see_prereq):
"""
This test asserts that proctoring will not be displayed under the following
conditions:
......@@ -773,9 +812,6 @@ class ProctoredExamApiTests(LoggedInTestCase):
exam = get_exam_by_id(self.proctored_exam_id)
if pre_create_attempt:
create_exam_attempt(self.proctored_exam_id, self.user_id)
# user hasn't attempted reverifications
rendered_response = get_student_view(
user_id=self.user_id,
......@@ -791,22 +827,20 @@ class ProctoredExamApiTests(LoggedInTestCase):
'credit_requirement_status': [
{
'namespace': namespace,
'name': 'foo',
'display_name': 'Foo Requirement',
'status': req_status,
'order': 0
}
]
}
}
)
if show_proctored:
self.assertIsNotNone(rendered_response)
else:
self.assertIsNone(rendered_response)
# also the user should have been marked as declined in certain
# cases
if mark_as_declined:
attempt = get_exam_attempt(self.proctored_exam_id, self.user_id)
self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.declined)
self.assertIn(expected_content, rendered_response)
if should_see_prereq:
self.assertIn('Foo Requirement', rendered_response)
def test_student_view_non_student(self):
"""
......@@ -2010,3 +2044,67 @@ class ProctoredExamApiTests(LoggedInTestCase):
)
self.assertIsNotNone(rendered_response)
self.assertIn(self.footer_msg, rendered_response)
def test_requirement_status_order(self):
"""
Make sure that we get a correct ordered list of all statuses sorted in the correct
order
"""
# try unfiltered version first
ordered_list = _get_ordered_prerequisites(self.prerequisites)
self.assertEqual(len(ordered_list), 5)
# check the ordering
for idx in range(5):
self.assertEqual(ordered_list[idx]['order'], idx)
# now filter out the 'grade' namespace
ordered_list = _get_ordered_prerequisites(self.prerequisites, ['grade'])
self.assertEqual(len(ordered_list), 4)
# check the ordering
for idx in range(4):
# we +1 on the idx because we know we filtered out one
self.assertEqual(ordered_list[idx]['order'], idx + 1)
# check other expected ordering
self.assertEqual(ordered_list[0]['namespace'], 'reverification')
self.assertEqual(ordered_list[0]['name'], 'rever1')
self.assertEqual(ordered_list[1]['namespace'], 'proctoring')
self.assertEqual(ordered_list[1]['name'], 'proc1')
self.assertEqual(ordered_list[2]['namespace'], 'reverification')
self.assertEqual(ordered_list[2]['name'], 'rever2')
self.assertEqual(ordered_list[3]['namespace'], 'proctoring')
self.assertEqual(ordered_list[3]['name'], 'proc2')
@ddt.data(
('rever1', True, 0, 0, 0),
('proc1', True, 1, 0, 0),
('rever2', True, 2, 0, 0),
('proc2', False, 2, 1, 0),
('unknown', False, 2, 1, 1),
(None, False, 2, 1, 1),
)
@ddt.unpack
def test_are_prerequisite_satisifed(self, content_id,
expected_are_prerequisites_satisifed,
expected_len_satisfied_prerequisites,
expected_len_failed_prerequisites,
expected_len_pending_prerequisites):
"""
verify proper operation of the logic when computing is prerequisites are satisfied
"""
results = _are_prerequirements_satisfied(
self.prerequisites,
content_id,
filter_out_namespaces=['grade']
)
self.assertEqual(results['are_prerequisites_satisifed'], expected_are_prerequisites_satisifed)
self.assertEqual(len(results['satisfied_prerequisites']), expected_len_satisfied_prerequisites)
self.assertEqual(len(results['failed_prerequisites']), expected_len_failed_prerequisites)
self.assertEqual(len(results['pending_prerequisites']), expected_len_pending_prerequisites)
......@@ -22,6 +22,7 @@ class MockCreditService(object):
"""
Initializer
"""
self.order = 0
self.status = {
'course_name': course_name,
'enrollment_mode': enrollment_mode,
......@@ -57,11 +58,14 @@ class MockCreditService(object):
'req_namespace': req_namespace,
'namespace': req_namespace,
'name': req_name,
'status': status
'status': status,
'order': self.order,
})
else:
found[0]['status'] = status
self.order = self.order + 1
# pylint: disable=unused-argument
# pylint: disable=invalid-name
def remove_credit_requirement_status(self, user_id, course_key_or_id, req_namespace, req_name):
......
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