Commit 955282f9 by Eric Fischer

Merge pull request #12165 from edx/efischer/hide_timed_exams

TNL-4366 Hide Timed Exams
parents bc28df90 ea77c3ec
...@@ -83,7 +83,8 @@ def register_special_exams(course_key): ...@@ -83,7 +83,8 @@ def register_special_exams(course_key):
due_date=timed_exam.due, due_date=timed_exam.due,
is_proctored=timed_exam.is_proctored_exam, is_proctored=timed_exam.is_proctored_exam,
is_practice_exam=timed_exam.is_practice_exam, is_practice_exam=timed_exam.is_practice_exam,
is_active=True is_active=True,
hide_after_due=timed_exam.hide_after_due,
) )
msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id']) msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id'])
log.info(msg) log.info(msg)
...@@ -97,7 +98,8 @@ def register_special_exams(course_key): ...@@ -97,7 +98,8 @@ def register_special_exams(course_key):
due_date=timed_exam.due, due_date=timed_exam.due,
is_proctored=timed_exam.is_proctored_exam, is_proctored=timed_exam.is_proctored_exam,
is_practice_exam=timed_exam.is_practice_exam, is_practice_exam=timed_exam.is_practice_exam,
is_active=True is_active=True,
hide_after_due=timed_exam.hide_after_due,
) )
msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id) msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id)
log.info(msg) log.info(msg)
......
...@@ -53,6 +53,10 @@ class TestProctoredExams(ModuleStoreTestCase): ...@@ -53,6 +53,10 @@ class TestProctoredExams(ModuleStoreTestCase):
exam_review_policy = get_review_policy_by_exam_id(exam['id']) exam_review_policy = get_review_policy_by_exam_id(exam['id'])
self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules) self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules)
if not exam['is_proctored'] and not exam['is_practice_exam']:
# the hide after due value only applies to timed exams
self.assertEqual(exam['hide_after_due'], sequence.hide_after_due)
self.assertEqual(exam['course_id'], unicode(self.course.id)) self.assertEqual(exam['course_id'], unicode(self.course.id))
self.assertEqual(exam['content_id'], unicode(sequence.location)) self.assertEqual(exam['content_id'], unicode(sequence.location))
self.assertEqual(exam['exam_name'], sequence.display_name) self.assertEqual(exam['exam_name'], sequence.display_name)
...@@ -62,13 +66,14 @@ class TestProctoredExams(ModuleStoreTestCase): ...@@ -62,13 +66,14 @@ class TestProctoredExams(ModuleStoreTestCase):
self.assertEqual(exam['is_active'], expected_active) self.assertEqual(exam['is_active'], expected_active)
@ddt.data( @ddt.data(
(True, 10, True, False, True, False), (True, 10, True, False, True, False, False),
(True, 10, False, False, True, False), (True, 10, False, False, True, False, False),
(True, 10, True, True, True, True), (True, 10, False, False, True, False, True),
(True, 10, True, True, True, True, False),
) )
@ddt.unpack @ddt.unpack
def test_publishing_exam(self, is_time_limited, default_time_limit_minutes, def test_publishing_exam(self, is_time_limited, default_time_limit_minutes, is_proctored_exam,
is_proctored_exam, is_practice_exam, expected_active, republish): is_practice_exam, expected_active, republish, hide_after_due):
""" """
Happy path testing to see that when a course is published which contains Happy path testing to see that when a course is published which contains
a proctored exam, it will also put an entry into the exam tables a proctored exam, it will also put an entry into the exam tables
...@@ -85,7 +90,8 @@ class TestProctoredExams(ModuleStoreTestCase): ...@@ -85,7 +90,8 @@ class TestProctoredExams(ModuleStoreTestCase):
is_proctored_exam=is_proctored_exam, is_proctored_exam=is_proctored_exam,
is_practice_exam=is_practice_exam, is_practice_exam=is_practice_exam,
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1), due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1),
exam_review_rules="allow_use_of_paper" exam_review_rules="allow_use_of_paper",
hide_after_due=hide_after_due,
) )
listen_for_course_publish(self, self.course.id) listen_for_course_publish(self, self.course.id)
...@@ -117,7 +123,8 @@ class TestProctoredExams(ModuleStoreTestCase): ...@@ -117,7 +123,8 @@ class TestProctoredExams(ModuleStoreTestCase):
graded=True, graded=True,
is_time_limited=True, is_time_limited=True,
default_time_limit_minutes=10, default_time_limit_minutes=10,
is_proctored_exam=True is_proctored_exam=True,
hide_after_due=False,
) )
listen_for_course_publish(self, self.course.id) listen_for_course_publish(self, self.course.id)
...@@ -147,7 +154,8 @@ class TestProctoredExams(ModuleStoreTestCase): ...@@ -147,7 +154,8 @@ class TestProctoredExams(ModuleStoreTestCase):
graded=True, graded=True,
is_time_limited=True, is_time_limited=True,
default_time_limit_minutes=10, default_time_limit_minutes=10,
is_proctored_exam=True is_proctored_exam=True,
hide_after_due=False,
) )
listen_for_course_publish(self, self.course.id) listen_for_course_publish(self, self.course.id)
...@@ -182,7 +190,8 @@ class TestProctoredExams(ModuleStoreTestCase): ...@@ -182,7 +190,8 @@ class TestProctoredExams(ModuleStoreTestCase):
graded=True, graded=True,
is_time_limited=True, is_time_limited=True,
default_time_limit_minutes=10, default_time_limit_minutes=10,
is_proctored_exam=True is_proctored_exam=True,
hide_after_due=False,
) )
listen_for_course_publish(self, self.course.id) listen_for_course_publish(self, self.course.id)
...@@ -218,7 +227,8 @@ class TestProctoredExams(ModuleStoreTestCase): ...@@ -218,7 +227,8 @@ class TestProctoredExams(ModuleStoreTestCase):
is_time_limited=True, is_time_limited=True,
default_time_limit_minutes=10, default_time_limit_minutes=10,
is_proctored_exam=True, is_proctored_exam=True,
exam_review_rules="allow_use_of_paper" exam_review_rules="allow_use_of_paper",
hide_after_due=False,
) )
listen_for_course_publish(self, self.course.id) listen_for_course_publish(self, self.course.id)
......
...@@ -935,7 +935,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -935,7 +935,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"is_practice_exam": xblock.is_practice_exam, "is_practice_exam": xblock.is_practice_exam,
"is_time_limited": xblock.is_time_limited, "is_time_limited": xblock.is_time_limited,
"exam_review_rules": xblock.exam_review_rules, "exam_review_rules": xblock.exam_review_rules,
"default_time_limit_minutes": xblock.default_time_limit_minutes "default_time_limit_minutes": xblock.default_time_limit_minutes,
"hide_after_due": xblock.hide_after_due,
}) })
# Update with gating info # Update with gating info
......
...@@ -50,6 +50,7 @@ class CourseMetadata(object): ...@@ -50,6 +50,7 @@ class CourseMetadata(object):
'is_time_limited', 'is_time_limited',
'is_practice_exam', 'is_practice_exam',
'exam_review_rules', 'exam_review_rules',
'hide_after_due',
'self_paced', 'self_paced',
'chrome', 'chrome',
'default_tab', 'default_tab',
......
...@@ -332,41 +332,47 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -332,41 +332,47 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
templateName: 'timed-examination-preference-editor', templateName: 'timed-examination-preference-editor',
className: 'edit-settings-timed-examination', className: 'edit-settings-timed-examination',
events : { events : {
'change #id_not_timed': 'notTimedExam', 'change input.no_special_exam': 'notTimedExam',
'change #id_timed_exam': 'setTimedExam', 'change input.timed_exam': 'setTimedExam',
'change #id_practice_exam': 'setPracticeExam', 'change input.practice_exam': 'setPracticeExam',
'change #id_proctored_exam': 'setProctoredExam', 'change input.proctored_exam': 'setProctoredExam',
'focusout #id_time_limit': 'timeLimitFocusout' 'focusout .field-time-limit input': 'timeLimitFocusout'
}, },
notTimedExam: function (event) { notTimedExam: function (event) {
event.preventDefault(); event.preventDefault();
this.$('#id_time_limit_div').hide(); this.$('.exam-options').hide();
this.$('.exam-review-rules-list-fields').hide(); this.$('.field-time-limit input').val('00:00');
this.$('#id_time_limit').val('00:00'); },
}, selectSpecialExam: function (showRulesField, showHideAfterDueField) {
selectSpecialExam: function (showRulesField) { this.$('.exam-options').show();
this.$('#id_time_limit_div').show(); this.$('.field-time-limit').show();
if (!this.isValidTimeLimit(this.$('#id_time_limit').val())) { if (!this.isValidTimeLimit(this.$('.field-time-limit input').val())) {
this.$('#id_time_limit').val('00:30'); this.$('.field-time-limit input').val('00:30');
} }
if (showRulesField) { if (showRulesField) {
this.$('.exam-review-rules-list-fields').show(); this.$('.field-exam-review-rules').show();
} }
else { else {
this.$('.exam-review-rules-list-fields').hide(); this.$('.field-exam-review-rules').hide();
}
if (showHideAfterDueField) {
this.$('.field-hide-after-due').show();
}
else {
this.$('.field-hide-after-due').hide();
} }
}, },
setTimedExam: function (event) { setTimedExam: function (event) {
event.preventDefault(); event.preventDefault();
this.selectSpecialExam(false); this.selectSpecialExam(false, true);
}, },
setPracticeExam: function (event) { setPracticeExam: function (event) {
event.preventDefault(); event.preventDefault();
this.selectSpecialExam(false); this.selectSpecialExam(false, false);
}, },
setProctoredExam: function (event) { setProctoredExam: function (event) {
event.preventDefault(); event.preventDefault();
this.selectSpecialExam(true); this.selectSpecialExam(true, false);
}, },
timeLimitFocusout: function(event) { timeLimitFocusout: function(event) {
event.preventDefault(); event.preventDefault();
...@@ -389,43 +395,51 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -389,43 +395,51 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
this.setExamTime(this.model.get('default_time_limit_minutes')); this.setExamTime(this.model.get('default_time_limit_minutes'));
this.setReviewRules(this.model.get('exam_review_rules')); this.setReviewRules(this.model.get('exam_review_rules'));
this.setHideAfterDue(this.model.get('hide_after_due'));
}, },
setExamType: function(is_time_limited, is_proctored_exam, is_practice_exam) { setExamType: function(is_time_limited, is_proctored_exam, is_practice_exam) {
this.$('.field-time-limit').hide();
this.$('.field-exam-review-rules').hide();
this.$('.field-hide-after-due').hide();
if (!is_time_limited) { if (!is_time_limited) {
this.$("#id_not_timed").prop('checked', true); this.$('input.no_special_exam').prop('checked', true);
return; return;
} }
this.$('#id_time_limit_div').show(); this.$('.field-time-limit').show();
this.$('.exam-review-rules-list-fields').hide();
if (this.options.enable_proctored_exams && is_proctored_exam) { if (this.options.enable_proctored_exams && is_proctored_exam) {
if (is_practice_exam) { if (is_practice_exam) {
this.$('#id_practice_exam').prop('checked', true); this.$('input.practice_exam').prop('checked', true);
} else { } else {
this.$('#id_proctored_exam').prop('checked', true); this.$('input.proctored_exam').prop('checked', true);
this.$('.exam-review-rules-list-fields').show(); this.$('.field-exam-review-rules').show();
} }
} else { } else {
// Since we have an early exit at the top of the method // Since we have an early exit at the top of the method
// if the subsection is not time limited, then // if the subsection is not time limited, then
// here we rightfully assume that it just a timed exam // here we rightfully assume that it just a timed exam
this.$("#id_timed_exam").prop('checked', true); this.$('input.timed_exam').prop('checked', true);
this.$('.field-hide-after-due').show();
} }
}, },
setExamTime: function(value) { setExamTime: function(value) {
var time = this.convertTimeLimitMinutesToString(value); var time = this.convertTimeLimitMinutesToString(value);
this.$('#id_time_limit').val(time); this.$('.field-time-limit input').val(time);
}, },
setReviewRules: function (value) { setReviewRules: function (value) {
this.$('#id_exam_review_rules').val(value); this.$('.field-exam-review-rules textarea').val(value);
},
setHideAfterDue: function(value) {
this.$('.field-hide-after-due input').prop('checked', value);
}, },
isValidTimeLimit: function(time_limit) { isValidTimeLimit: function(time_limit) {
var pattern = new RegExp('^\\d{1,2}:[0-5][0-9]$'); var pattern = new RegExp('^\\d{1,2}:[0-5][0-9]$');
return pattern.test(time_limit) && time_limit !== "00:00"; return pattern.test(time_limit) && time_limit !== "00:00";
}, },
getExamTimeLimit: function () { getExamTimeLimit: function () {
return this.$('#id_time_limit').val(); return this.$('.field-time-limit input').val();
}, },
convertTimeLimitMinutesToString: function (timeLimitMinutes) { convertTimeLimitMinutesToString: function (timeLimitMinutes) {
var hoursStr = "" + Math.floor(timeLimitMinutes / 60); var hoursStr = "" + Math.floor(timeLimitMinutes / 60);
...@@ -444,21 +458,22 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -444,21 +458,22 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var is_practice_exam; var is_practice_exam;
var is_proctored_exam; var is_proctored_exam;
var time_limit = this.getExamTimeLimit(); var time_limit = this.getExamTimeLimit();
var exam_review_rules = this.$('#id_exam_review_rules').val(); var exam_review_rules = this.$('.field-exam-review-rules textarea').val();
var hide_after_due = this.$('.field-hide-after-due input').is(':checked');
if (this.$("#id_not_timed").is(':checked')){ if (this.$('input.no_special_exam').is(':checked')){
is_time_limited = false; is_time_limited = false;
is_practice_exam = false; is_practice_exam = false;
is_proctored_exam = false; is_proctored_exam = false;
} else if (this.$("#id_timed_exam").is(':checked')){ } else if (this.$('input.timed_exam').is(':checked')){
is_time_limited = true; is_time_limited = true;
is_practice_exam = false; is_practice_exam = false;
is_proctored_exam = false; is_proctored_exam = false;
} else if (this.$("#id_proctored_exam").is(':checked')){ } else if (this.$('input.proctored_exam').is(':checked')){
is_time_limited = true; is_time_limited = true;
is_practice_exam = false; is_practice_exam = false;
is_proctored_exam = true; is_proctored_exam = true;
} else if (this.$("#id_practice_exam").is(':checked')){ } else if (this.$('input.practice_exam').is(':checked')){
is_time_limited = true; is_time_limited = true;
is_practice_exam = true; is_practice_exam = true;
is_proctored_exam = true; is_proctored_exam = true;
...@@ -469,6 +484,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', ...@@ -469,6 +484,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
'is_practice_exam': is_practice_exam, 'is_practice_exam': is_practice_exam,
'is_time_limited': is_time_limited, 'is_time_limited': is_time_limited,
'exam_review_rules': exam_review_rules, 'exam_review_rules': exam_review_rules,
'hide_after_due': hide_after_due,
// We have to use the legacy field name // We have to use the legacy field name
// as the Ajax handler directly populates // as the Ajax handler directly populates
// the xBlocks fields. We will have to // the xBlocks fields. We will have to
......
...@@ -564,7 +564,7 @@ ...@@ -564,7 +564,7 @@
} }
.list-fields { .list-fields {
.field-message { .field-message {
color: $gray; color: $gray-d1;
font-size: ($baseline/2); font-size: ($baseline/2);
} }
.field { .field {
......
...@@ -303,7 +303,7 @@ $outline-indent-width: $baseline; ...@@ -303,7 +303,7 @@ $outline-indent-width: $baseline;
%outline-item-status { %outline-item-status {
@extend %t-copy-sub2; @extend %t-copy-sub2;
@extend %t-strong; @extend %t-strong;
color: $color-copy-base; color: $gray-d1;
.icon { .icon {
@extend %t-icon5; @extend %t-icon5;
...@@ -576,12 +576,16 @@ $outline-indent-width: $baseline; ...@@ -576,12 +576,16 @@ $outline-indent-width: $baseline;
> .subsection-status .status-timed-proctored-exam { > .subsection-status .status-timed-proctored-exam {
opacity: 1.0; opacity: 1.0;
} }
> .subsection-status .status-hide-after-due {
opacity: 1.0;
}
} }
// status - grading // status - grading
.status-grading, .status-timed-proctored-exam { .status-grading, .status-timed-proctored-exam, .status-hide-after-due {
@include transition(opacity $tmg-f2 ease-in-out 0s); @include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65; opacity: 0.75;
} }
.status-grading-value, .status-proctored-exam-value { .status-grading-value, .status-proctored-exam-value {
......
<form> <form>
<h3 class="modal-section-title"><%= gettext('Set as a Special Exam') %></h3> <h3 class="modal-section-title"><%= gettext('Set as a Special Exam') %></h3>
<div class="modal-section-content has-actions"> <div class="modal-section-content has-actions">
<div class='exam-time-list-fields'> <ul class="list-fields list-input exam-types" role="group" aria-label="<%- gettext('Exam Types') %>">
<ul class="list-fields list-input"> <li class="field-radio">
<label class="label">
<input type="radio" name="exam_type" class="input input-radio no_special_exam" checked="checked"/>
<%- gettext('None') %>
</label>
</li>
<li class="field-radio">
<label class="label">
<input type="radio" name="exam_type" class="input input-radio timed_exam"
aria-describedby="timed-exam-description" />
<%- gettext('Timed') %>
</label>
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the Instructor Dashboard.') %> </p>
</li>
<% if (enable_proctored_exam) { %>
<li class="field-radio"> <li class="field-radio">
<input type="radio" id="id_not_timed" name="proctored" class="input input-radio" checked="checked"/> <label class="label">
<label for="id_not_timed" class="label"> <input type="radio" name="exam_type" class="input input-radio proctored_exam"
<%- gettext('None') %> aria-describedby="proctored-exam-description" />
<%- gettext('Proctored') %>
</label> </label>
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules.') %> </p>
</li> </li>
</ul>
</div>
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<li class="field-radio"> <li class="field-radio">
<input type="radio" id="id_timed_exam" name="proctored" class="input input-radio" /> <label class="label">
<label for="id_timed_exam" class="label" aria-describedby="timed-exam-description"> <input type="radio" name="exam_type" class="input input-radio practice_exam"
<%- gettext('Timed') %> aria-describedby="practice-exam-description"/>
<%- gettext('Practice Proctored') %>
</label> </label>
</li>
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the Instructor Dashboard.') %> </p>
</ul>
</div>
<% if (enable_proctored_exam) { %>
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<li class="field-radio">
<input type="radio" id="id_proctored_exam" name="proctored" class="input input-radio" />
<label for="id_proctored_exam" class="label" aria-describedby="proctored-exam-description">
<%- gettext('Proctored') %>
</label>
</li>
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules.') %> </p>
</ul>
</div>
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<li class="field-radio">
<input type="radio" id="id_practice_exam" name="proctored" class="input input-radio" />
<label for="id_practice_exam" class="label" aria-describedby="practice-exam-description">
<%- gettext('Practice Proctored') %>
</label>
</li>
<p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner's grade.") %> </p> <p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner's grade.") %> </p>
</ul>
</div>
<% } %>
<div class='exam-time-list-fields is-hidden' id='id_time_limit_div'>
<ul class="list-fields list-input time-limit">
<li class="field field-text field-time-limit">
<label for="id_time_limit" class="label"><%- gettext('Time Allotted (HH:MM):') %> </label>
<input type="text" id="id_time_limit" name="time_limit"
value="" aria-describedby="time-limit-description"
placeholder="HH:MM" class="time_limit release-time time input input-text" autocomplete="off" />
</li> </li>
<% } %>
</ul>
<ul class="list-fields list-input exam-options">
<li class="field field-text field-time-limit">
<label class="label">
<%- gettext('Time Allotted (HH:MM):') %>
<input type="text" value="" aria-describedby="time-limit-description" placeholder="HH:MM"
class="time_limit release-time time input input-text" autocomplete="off" />
</label>
<p class='field-message' id='time-limit-description'><%- gettext('Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.') %></p> <p class='field-message' id='time-limit-description'><%- gettext('Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.') %></p>
</ul> </li>
</div> <li class="field field-text field-exam-review-rules">
<div class='exam-review-rules-list-fields is-hidden'> <label class="label">
<ul class="list-fields list-input exam-review-rules"> <%- gettext('Review Rules') %>
<li class="field field-text field-exam-review-rules"> <textarea cols="50" maxlength="255" aria-describedby="review-rules-description"
<label for="id_exam_review_rules" class="label"><%- gettext('Review Rules') %> </label>
<textarea id="id_exam_review_rules" cols="50" maxlength="255" name="review_rules" aria-describedby="review-rules-description"
class="review-rules input input-text" autocomplete="off" /> class="review-rules input input-text" autocomplete="off" />
</li> </label>
<p class='field-message' id='review-rules-description'><%- gettext('Specify any additional rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed.') %></p> <p class='field-message' id='review-rules-description'><%- gettext('Specify any additional rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed.') %></p>
</ul> </li>
</div> <li class="field-checkbox field-hide-after-due">
<label class="label">
<input type="checkbox" class="input input-checkbox"
aria-describedby="hide-after-due-description"/>
<%- gettext('Hide Exam After Due Date') %>
</label>
<p class='field-message' id='hide-after-due-description'><%- gettext('By default, submitted exams are available for review after the due date has passed. Select this option to keep exams hidden after that date.') %></p>
</li>
</ul>
</div> </div>
</form> </form>
...@@ -97,6 +97,16 @@ class ProctoringFields(object): ...@@ -97,6 +97,16 @@ class ProctoringFields(object):
scope=Scope.settings, scope=Scope.settings,
) )
hide_after_due = Boolean(
display_name=_("Hide Exam Results After Due Date"),
help=_(
"This setting overrides the default behavior of showing exam results after the due date has passed."
" Currently only supported for timed exams."
),
default=False,
scope=Scope.settings,
)
is_practice_exam = Boolean( is_practice_exam = Boolean(
display_name=_("Is Practice Exam"), display_name=_("Is Practice Exam"),
help=_( help=_(
......
...@@ -200,7 +200,14 @@ class CoursewarePage(CoursePage): ...@@ -200,7 +200,14 @@ class CoursewarePage(CoursePage):
self.q(css='button.start-timed-exam[data-start-immediately="false"]').first.click() self.q(css='button.start-timed-exam[data-start-immediately="false"]').first.click()
# Wait for the unique exam code to appear. # Wait for the unique exam code to appear.
# elf.wait_for_element_presence(".proctored-exam-code", "unique exam code") # self.wait_for_element_presence(".proctored-exam-code", "unique exam code")
def has_submitted_exam_message(self):
"""
Returns whether the "you have submitted your exam" message is present.
This being true implies "the exam contents and results are hidden".
"""
return self.q(css="div.proctored-exam.completed").visible
@property @property
def entrance_exam_message_selector(self): def entrance_exam_message_selector(self):
......
...@@ -549,7 +549,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -549,7 +549,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
self.q(css=".subsection-header-actions .configure-button").nth(index).click() self.q(css=".subsection-header-actions .configure-button").nth(index).click()
self.wait_for_element_presence('.course-outline-modal', 'Subsection settings modal is present.') self.wait_for_element_presence('.course-outline-modal', 'Subsection settings modal is present.')
def change_problem_release_date_in_studio(self): def change_problem_release_date(self):
""" """
Sets a new start date Sets a new start date
""" """
...@@ -558,26 +558,39 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -558,26 +558,39 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
self.q(css=".action-save").first.click() self.q(css=".action-save").first.click()
self.wait_for_ajax() self.wait_for_ajax()
def change_problem_due_date(self, date):
"""
Sets a new due date.
Expects date to be a string that will be accepted by the input (for example, '01/01/1970')
"""
self.q(css=".subsection-header-actions .configure-button").first.click()
self.q(css="#due_date").fill(date)
self.q(css=".action-save").first.click()
self.wait_for_ajax()
def select_advanced_tab(self): def select_advanced_tab(self):
""" """
Select the advanced settings tab Select the advanced settings tab
""" """
self.q(css=".settings-tab-button[data-tab='advanced']").first.click() self.q(css=".settings-tab-button[data-tab='advanced']").first.click()
self.wait_for_element_presence('#id_not_timed', 'Special exam settings fields not present.') self.wait_for_element_presence('input.no_special_exam', 'Special exam settings fields not present.')
def make_exam_proctored(self): def make_exam_proctored(self):
""" """
Makes a Proctored exam. Makes a Proctored exam.
""" """
self.q(css="#id_proctored_exam").first.click() self.q(css="input.proctored_exam").first.click()
self.q(css=".action-save").first.click() self.q(css=".action-save").first.click()
self.wait_for_ajax() self.wait_for_ajax()
def make_exam_timed(self): def make_exam_timed(self, hide_after_due=False):
""" """
Makes a timed exam. Makes a timed exam.
""" """
self.q(css="#id_timed_exam").first.click() self.q(css="input.timed_exam").first.click()
if hide_after_due:
self.q(css='.field-hide-after-due input').first.click()
self.q(css=".action-save").first.click() self.q(css=".action-save").first.click()
self.wait_for_ajax() self.wait_for_ajax()
...@@ -585,37 +598,43 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -585,37 +598,43 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
""" """
Choose "none" exam but do not press enter Choose "none" exam but do not press enter
""" """
self.q(css="#id_not_timed").first.click() self.q(css="input.no_special_exam").first.click()
def select_timed_exam(self): def select_timed_exam(self):
""" """
Choose a timed exam but do not press enter Choose a timed exam but do not press enter
""" """
self.q(css="#id_timed_exam").first.click() self.q(css="input.timed_exam").first.click()
def select_proctored_exam(self): def select_proctored_exam(self):
""" """
Choose a proctored exam but do not press enter Choose a proctored exam but do not press enter
""" """
self.q(css="#id_proctored_exam").first.click() self.q(css="input.proctored_exam").first.click()
def select_practice_exam(self): def select_practice_exam(self):
""" """
Choose a practice exam but do not press enter Choose a practice exam but do not press enter
""" """
self.q(css="#id_practice_exam").first.click() self.q(css="input.practice_exam").first.click()
def time_allotted_field_visible(self): def time_allotted_field_visible(self):
""" """
returns whether the time allotted field is visible returns whether the time allotted field is visible
""" """
return self.q(css="#id_time_limit_div").visible return self.q(css=".field-time-limit").visible
def exam_review_rules_field_visible(self): def exam_review_rules_field_visible(self):
""" """
Returns whether the review rules field is visible Returns whether the review rules field is visible
""" """
return self.q(css=".exam-review-rules-list-fields").visible return self.q(css=".field-exam-review-rules").visible
def hide_after_due_field_visible(self):
"""
Returns whether the hide after due field is visible
"""
return self.q(css=".field-hide-after-due").visible
def proctoring_items_are_displayed(self): def proctoring_items_are_displayed(self):
""" """
...@@ -623,19 +642,19 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -623,19 +642,19 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
""" """
# The None radio button # The None radio button
if not self.q(css="#id_not_timed").present: if not self.q(css="input.no_special_exam").present:
return False return False
# The Timed exam radio button # The Timed exam radio button
if not self.q(css="#id_timed_exam").present: if not self.q(css="input.timed_exam").present:
return False return False
# The Proctored exam radio button # The Proctored exam radio button
if not self.q(css="#id_proctored_exam").present: if not self.q(css="input.proctored_exam").present:
return False return False
# The Practice exam radio button # The Practice exam radio button
if not self.q(css="#id_practice_exam").present: if not self.q(css="input.practice_exam").present:
return False return False
return True return True
......
...@@ -5,6 +5,8 @@ End-to-end tests for the LMS. ...@@ -5,6 +5,8 @@ End-to-end tests for the LMS.
import json import json
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from datetime import datetime, timedelta
import ddt
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from ..helpers import UniqueCourseTest, EventsTestMixin from ..helpers import UniqueCourseTest, EventsTestMixin
...@@ -98,7 +100,7 @@ class CoursewareTest(UniqueCourseTest): ...@@ -98,7 +100,7 @@ class CoursewareTest(UniqueCourseTest):
self.course_outline.visit() self.course_outline.visit()
# Set release date for subsection in future. # Set release date for subsection in future.
self.course_outline.change_problem_release_date_in_studio() self.course_outline.change_problem_release_date()
# Logout and login as a student. # Logout and login as a student.
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
...@@ -127,6 +129,7 @@ class CoursewareTest(UniqueCourseTest): ...@@ -127,6 +129,7 @@ class CoursewareTest(UniqueCourseTest):
self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb) self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb)
@ddt.ddt
class ProctoredExamTest(UniqueCourseTest): class ProctoredExamTest(UniqueCourseTest):
""" """
Test courseware. Test courseware.
...@@ -246,7 +249,8 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -246,7 +249,8 @@ class ProctoredExamTest(UniqueCourseTest):
self.courseware_page.visit() self.courseware_page.visit()
self.assertTrue(self.courseware_page.can_start_proctored_exam) self.assertTrue(self.courseware_page.can_start_proctored_exam)
def test_timed_exam_flow(self): @ddt.data(True, False)
def test_timed_exam_flow(self, hide_after_due):
""" """
Given that I am a staff member on the exam settings section Given that I am a staff member on the exam settings section
select advanced settings tab select advanced settings tab
...@@ -255,6 +259,12 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -255,6 +259,12 @@ class ProctoredExamTest(UniqueCourseTest):
And visit the courseware as a verified student. And visit the courseware as a verified student.
And I start the timed exam And I start the timed exam
Then I am taken to the exam with a timer bar showing Then I am taken to the exam with a timer bar showing
When I finish the exam
Then I see the exam submitted dialog in place of the exam
When I log back into studio as a staff member
And change the problem's due date to be in the past
And log back in as the original verified student
Then I see the exam or message in accordance with the hide_after_due setting
""" """
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True) self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
...@@ -262,7 +272,7 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -262,7 +272,7 @@ class ProctoredExamTest(UniqueCourseTest):
self.course_outline.open_subsection_settings_dialog() self.course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab() self.course_outline.select_advanced_tab()
self.course_outline.make_exam_timed() self.course_outline.make_exam_timed(hide_after_due=hide_after_due)
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._login_as_a_verified_user() self._login_as_a_verified_user()
...@@ -271,94 +281,32 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -271,94 +281,32 @@ class ProctoredExamTest(UniqueCourseTest):
self.courseware_page.start_timed_exam() self.courseware_page.start_timed_exam()
self.assertTrue(self.courseware_page.is_timer_bar_present) self.assertTrue(self.courseware_page.is_timer_bar_present)
def test_time_allotted_field_is_not_visible_with_none_exam(self): self.courseware_page.stop_timed_exam()
""" self.assertTrue(self.courseware_page.has_submitted_exam_message())
Given that I am a staff member
And I have visited the course outline page in studio.
And the subsection edit dialog is open
select advanced settings tab
When I select the 'None' exams radio button
Then the time allotted text field becomes invisible
"""
LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
self.course_outline.visit()
self.course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab()
self.course_outline.select_none_exam()
self.assertFalse(self.course_outline.time_allotted_field_visible())
def test_time_allotted_field_is_visible_with_timed_exam(self):
"""
Given that I am a staff member
And I have visited the course outline page in studio.
And the subsection edit dialog is open
select advanced settings tab
When I select the timed exams radio button
Then the time allotted text field becomes visible
"""
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True) self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
self.course_outline.visit() self.course_outline.visit()
last_week = (datetime.today() - timedelta(days=7)).strftime("%m/%d/%Y")
self.course_outline.change_problem_due_date(last_week)
self.course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab()
self.course_outline.select_timed_exam()
self.assertTrue(self.course_outline.time_allotted_field_visible())
def test_time_allotted_field_is_visible_with_proctored_exam(self):
"""
Given that I am a staff member
And I have visited the course outline page in studio.
And the subsection edit dialog is open
select advanced settings tab
When I select the proctored exams radio button
Then the time allotted text field becomes visible
"""
LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
self.course_outline.visit()
self.course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab()
self.course_outline.select_proctored_exam()
self.assertTrue(self.course_outline.time_allotted_field_visible())
def test_exam_review_rules_field_is_visible_with_proctored_exam(self):
"""
Given that I am a staff member
And I have visited the course outline page in studio.
And the subsection edit dialog is open
select advanced settings tab
When I select the proctored exams radio button
Then the review rules textarea field becomes visible
"""
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True) self._auto_auth(self.USERNAME, self.EMAIL, False)
self.course_outline.visit() self.courseware_page.visit()
self.assertEqual(self.courseware_page.has_submitted_exam_message(), hide_after_due)
self.course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab()
self.course_outline.select_proctored_exam()
self.assertTrue(self.course_outline.exam_review_rules_field_visible())
def test_exam_review_rules_field_is_not_visible_with_other_than_proctored_exam(self): def test_field_visiblity_with_all_exam_types(self):
""" """
Given that I am a staff member Given that I am a staff member
And I have visited the course outline page in studio. And I have visited the course outline page in studio.
And the subsection edit dialog is open And the subsection edit dialog is open
select advanced settings tab select advanced settings tab
When I select the timed exams radio button For each of None, Timed, Proctored, and Practice exam types
Then the review rules textarea field is not visible The time allotted, review rules, and hide after due fields have proper visibility
When I select the none exam radio button None: False, False, False
Then the review rules textarea field is not visible Timed: True, False, True
When I select the practice exam radio button Proctored: True, True, False
Then the review rules textarea field is not visible Practice: True, False, False
""" """
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True) self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
...@@ -367,33 +315,25 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -367,33 +315,25 @@ class ProctoredExamTest(UniqueCourseTest):
self.course_outline.open_subsection_settings_dialog() self.course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab() self.course_outline.select_advanced_tab()
self.course_outline.select_timed_exam()
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
self.course_outline.select_none_exam() self.course_outline.select_none_exam()
self.assertFalse(self.course_outline.time_allotted_field_visible())
self.assertFalse(self.course_outline.exam_review_rules_field_visible()) self.assertFalse(self.course_outline.exam_review_rules_field_visible())
self.assertFalse(self.course_outline.hide_after_due_field_visible())
self.course_outline.select_practice_exam() self.course_outline.select_timed_exam()
self.assertTrue(self.course_outline.time_allotted_field_visible())
self.assertFalse(self.course_outline.exam_review_rules_field_visible()) self.assertFalse(self.course_outline.exam_review_rules_field_visible())
self.assertTrue(self.course_outline.hide_after_due_field_visible())
def test_time_allotted_field_is_visible_with_practice_exam(self): self.course_outline.select_proctored_exam()
""" self.assertTrue(self.course_outline.time_allotted_field_visible())
Given that I am a staff member self.assertTrue(self.course_outline.exam_review_rules_field_visible())
And I have visited the course outline page in studio. self.assertFalse(self.course_outline.hide_after_due_field_visible())
And the subsection edit dialog is open
select advanced settings tab
When I select the practice exams radio button
Then the time allotted text field becomes visible
"""
LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
self.course_outline.visit()
self.course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab()
self.course_outline.select_practice_exam() self.course_outline.select_practice_exam()
self.assertTrue(self.course_outline.time_allotted_field_visible()) self.assertTrue(self.course_outline.time_allotted_field_visible())
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
self.assertFalse(self.course_outline.hide_after_due_field_visible())
class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin): class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
Acceptance tests for Studio's Setting pages Acceptance tests for Studio's Setting pages
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import os
from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from base_studio_test import StudioCourseTest from base_studio_test import StudioCourseTest
...@@ -508,3 +511,63 @@ class StudioSettingsA11yTest(StudioCourseTest): ...@@ -508,3 +511,63 @@ class StudioSettingsA11yTest(StudioCourseTest):
}) })
self.settings_page.a11y_audit.check_for_accessibility_errors() self.settings_page.a11y_audit.check_for_accessibility_errors()
@attr('a11y')
class StudioSubsectionSettingsA11yTest(StudioCourseTest):
"""
Class to test accessibility on the subsection settings modals.
"""
def setUp(self): # pylint: disable=arguments-differ
browser = os.environ.get('SELENIUM_BROWSER', 'firefox')
# This test will fail if run using phantomjs < 2.0, due to an issue with bind()
# See https://github.com/ariya/phantomjs/issues/10522 for details.
# The course_outline uses this function, and as such will not fully load when run
# under phantomjs 1.9.8. So, to prevent this test from timing out at course_outline.visit(),
# force the use of firefox vs the standard a11y test usage of phantomjs 1.9.8.
# TODO: remove this block once https://openedx.atlassian.net/browse/TE-1047 is resolved.
if browser == 'phantomjs':
browser = 'firefox'
with patch.dict(os.environ, {'SELENIUM_BROWSER': browser}):
super(StudioSubsectionSettingsA11yTest, self).setUp(is_staff=True)
self.course_outline = CourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
def populate_course_fixture(self, course_fixture):
course_fixture.add_advanced_settings({
"enable_proctored_exams": {"value": "true"}
})
course_fixture.add_children(
XBlockFixtureDesc('chapter', 'Test Section 1').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children(
XBlockFixtureDesc('problem', 'Test Problem 1')
)
)
)
def test_special_exams_menu_a11y(self):
"""
Given that I am a staff member
And I am editing settings on the special exams menu
Then that menu is accessible
"""
self.course_outline.visit()
self.course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab()
# limit the scope of the audit to the special exams tab on the modal dialog
self.course_outline.a11y_audit.config.set_scope(
include=['section.edit-settings-timed-examination']
)
self.course_outline.a11y_audit.check_for_accessibility_errors()
...@@ -89,8 +89,8 @@ git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2 ...@@ -89,8 +89,8 @@ git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5 -e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5
git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1 git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1
git+https://github.com/edx/edx-proctoring.git@0.12.15#egg=edx-proctoring==0.12.15
git+https://github.com/edx/xblock-lti-consumer.git@v1.0.6#egg=xblock-lti-consumer==1.0.6 git+https://github.com/edx/xblock-lti-consumer.git@v1.0.6#egg=xblock-lti-consumer==1.0.6
git+https://github.com/edx/edx-proctoring.git@0.12.16#egg=edx-proctoring==0.12.16
# Third Party XBlocks # Third Party XBlocks
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga -e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
......
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