Hide After Due setting for Timed Exams
TNL-4366
Changes in studio to allow the hide_after_due setting to be utilized.
Includes:
-python changes to contentstore, where the data is stored.
-refactoring of timed-examination-preference-editor.underscore, to add this
setting and make the editor more accessible.
-javascript changes to link the new setting to the correct data model.
-sass updates to fix a11y issues on the editor modal.
-addition of post-due visibility information to course outline in studio.
-new tests: python, js, acceptance, and a11y
This commit is contained in:
committed by
Clinton Blackburn
parent
58b6175580
commit
d48e88ba8a
@@ -97,6 +97,16 @@ class ProctoringFields(object):
|
||||
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(
|
||||
display_name=_("Is Practice Exam"),
|
||||
help=_(
|
||||
|
||||
@@ -200,7 +200,14 @@ class CoursewarePage(CoursePage):
|
||||
self.q(css='button.start-timed-exam[data-start-immediately="false"]').first.click()
|
||||
|
||||
# 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
|
||||
def entrance_exam_message_selector(self):
|
||||
|
||||
@@ -549,7 +549,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
|
||||
self.q(css=".subsection-header-actions .configure-button").nth(index).click()
|
||||
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
|
||||
"""
|
||||
@@ -558,26 +558,39 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
|
||||
self.q(css=".action-save").first.click()
|
||||
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):
|
||||
"""
|
||||
Select the advanced settings tab
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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.wait_for_ajax()
|
||||
|
||||
def make_exam_timed(self):
|
||||
def make_exam_timed(self, hide_after_due=False):
|
||||
"""
|
||||
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.wait_for_ajax()
|
||||
|
||||
@@ -585,37 +598,43 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
@@ -623,19 +642,19 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
|
||||
"""
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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 True
|
||||
|
||||
@@ -5,6 +5,8 @@ End-to-end tests for the LMS.
|
||||
|
||||
import json
|
||||
from nose.plugins.attrib import attr
|
||||
from datetime import datetime, timedelta
|
||||
import ddt
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from ..helpers import UniqueCourseTest, EventsTestMixin
|
||||
@@ -98,7 +100,7 @@ class CoursewareTest(UniqueCourseTest):
|
||||
self.course_outline.visit()
|
||||
|
||||
# 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.
|
||||
LogoutPage(self.browser).visit()
|
||||
@@ -127,6 +129,7 @@ class CoursewareTest(UniqueCourseTest):
|
||||
self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ProctoredExamTest(UniqueCourseTest):
|
||||
"""
|
||||
Test courseware.
|
||||
@@ -246,7 +249,8 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
self.courseware_page.visit()
|
||||
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
|
||||
select advanced settings tab
|
||||
@@ -255,6 +259,12 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
And visit the courseware as a verified student.
|
||||
And I start the timed exam
|
||||
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()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
@@ -262,7 +272,7 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
self.course_outline.open_subsection_settings_dialog()
|
||||
|
||||
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()
|
||||
self._login_as_a_verified_user()
|
||||
@@ -271,14 +281,32 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
self.courseware_page.start_timed_exam()
|
||||
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())
|
||||
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
last_week = (datetime.today() - timedelta(days=7)).strftime("%m/%d/%Y")
|
||||
self.course_outline.change_problem_due_date(last_week)
|
||||
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth(self.USERNAME, self.EMAIL, False)
|
||||
self.courseware_page.visit()
|
||||
self.assertEqual(self.courseware_page.has_submitted_exam_message(), hide_after_due)
|
||||
|
||||
def test_field_visiblity_with_all_exam_types(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 'None' exams radio button
|
||||
Then the time allotted text field becomes invisible
|
||||
For each of None, Timed, Proctored, and Practice exam types
|
||||
The time allotted, review rules, and hide after due fields have proper visibility
|
||||
None: False, False, False
|
||||
Timed: True, False, True
|
||||
Proctored: True, True, False
|
||||
Practice: True, False, False
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
@@ -289,111 +317,23 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
|
||||
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()
|
||||
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.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
self.assertFalse(self.course_outline.hide_after_due_field_visible())
|
||||
|
||||
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.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
self.assertTrue(self.course_outline.hide_after_due_field_visible())
|
||||
|
||||
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()
|
||||
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.exam_review_rules_field_visible())
|
||||
|
||||
def test_exam_review_rules_field_is_not_visible_with_other_than_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 timed exams radio button
|
||||
Then the review rules textarea field is not visible
|
||||
When I select the none exam radio button
|
||||
Then the review rules textarea field is not visible
|
||||
When I select the practice exam radio button
|
||||
Then the review rules textarea field is not 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_timed_exam()
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
|
||||
self.course_outline.select_none_exam()
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
|
||||
self.course_outline.select_practice_exam()
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
|
||||
def test_time_allotted_field_is_visible_with_practice_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 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.assertFalse(self.course_outline.hide_after_due_field_visible())
|
||||
|
||||
self.course_outline.select_practice_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.hide_after_due_field_visible())
|
||||
|
||||
|
||||
class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
Acceptance tests for Studio's Setting pages
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from base_studio_test import StudioCourseTest
|
||||
@@ -508,3 +511,63 @@ class StudioSettingsA11yTest(StudioCourseTest):
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user