diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index ffb5b48d7c..e164f45c80 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -1011,6 +1011,7 @@ class StudentAdminPage(PageObject): """ url = None EE_CONTAINER = ".entrance-exam-grade-container" + CS_CONTAINER = ".course-specific-container" def is_browser_on_page(self): """ @@ -1025,6 +1026,13 @@ class StudentAdminPage(PageObject): """ return self.q(css='{} input[name=entrance-exam-student-select-grade]'.format(self.EE_CONTAINER)) + @property + def rescore_problem_input(self): + """ + Returns input box for rescore/reset all on a problem + """ + return self.q(css='{} input[name=problem-select-all]'.format(self.CS_CONTAINER)) + @property def reset_attempts_button(self): """ @@ -1039,6 +1047,20 @@ class StudentAdminPage(PageObject): """ return self.q(css='{} input[name=rescore-entrance-exam]'.format(self.EE_CONTAINER)) + @property + def rescore_all_submissions_button(self): + """ + Returns rescore student submission button. + """ + return self.q(css='{} input[name=rescore-problem-all]'.format(self.CS_CONTAINER)) + + @property + def show_background_tasks_button(self): + """ + Return Show Background Tasks button. + """ + return self.q(css='{} input[name=task-history-all]'.format(self.CS_CONTAINER)) + @property def skip_entrance_exam_button(self): """ @@ -1115,6 +1137,18 @@ class StudentAdminPage(PageObject): """ return self.rescore_submission_button.click() + def click_rescore_all_button(self): + """ + clicks rescore all for problem button. + """ + return self.rescore_all_submissions_button.click() + + def click_show_background_tasks_button(self): + """ + clicks show background tasks button. + """ + return self.show_background_tasks_button.click() + def click_skip_entrance_exam_button(self): """ clicks let student skip entrance exam button. @@ -1133,12 +1167,19 @@ class StudentAdminPage(PageObject): """ return self.background_task_history_button.click() - def set_student_email(self, email_addres): + def set_student_email(self, email_address): """ Sets given email address as value of student email address/username input box. """ input_box = self.student_email_input.first.results[0] - input_box.send_keys(email_addres) + input_box.send_keys(email_address) + + def set_problem_to_rescore(self, problem_locator): + """ + Sets the problem for which to rescore/reset all scores. + """ + input_box = self.rescore_problem_input.first.results[0] + input_box.send_keys(problem_locator) class CertificatesPage(PageObject): diff --git a/common/test/acceptance/tests/lms/test_lms_courseware.py b/common/test/acceptance/tests/lms/test_lms_courseware.py index 84e2d3cb94..8e000d6e4b 100644 --- a/common/test/acceptance/tests/lms/test_lms_courseware.py +++ b/common/test/acceptance/tests/lms/test_lms_courseware.py @@ -14,9 +14,12 @@ from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from ..helpers import UniqueCourseTest, EventsTestMixin from ...pages.studio.auto_auth import AutoAuthPage from ...pages.lms.create_mode import ModeCreationPage +from ...pages.studio.component_editor import ComponentEditorView from ...pages.studio.overview import CourseOutlinePage +from ...pages.studio.utils import type_in_codemirror from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage from ...pages.lms.course_nav import CourseNavPage +from ...pages.lms.instructor_dashboard import InstructorDashboardPage from ...pages.lms.problem import ProblemPage from ...pages.common.logout import LogoutPage from ...pages.lms.staff_view import StaffPage @@ -912,16 +915,20 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest): self.assertEqual(self.progress_page.scores('Test Section 1', 'Test Subsection 1'), [(0, 1)]) -class ProgressPageTest(UniqueCourseTest): +class ProgressPageBaseTest(UniqueCourseTest): """ - Test that the progress page reports scores from completed assessments. + Provides utility methods for tests retrieving + scores from the progress page. """ USERNAME = "STUDENT_TESTER" EMAIL = "student101@example.com" + SECTION_NAME = 'Test Section 1' + SUBSECTION_NAME = 'Test Subsection 1' + UNIT_NAME = 'Test Unit 1' + PROBLEM_NAME = 'Test Problem 1' def setUp(self): - super(ProgressPageTest, self).setUp() - + super(ProgressPageBaseTest, self).setUp() self.courseware_page = CoursewarePage(self.browser, self.course_id) self.problem_page = ProblemPage(self.browser) # pylint: disable=attribute-defined-outside-init self.progress_page = ProgressPage(self.browser, self.course_id) @@ -943,9 +950,11 @@ class ProgressPageTest(UniqueCourseTest): ) course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children( - create_multiple_choice_problem('Test Problem 1') + XBlockFixtureDesc('chapter', self.SECTION_NAME).add_children( + XBlockFixtureDesc('sequential', self.SUBSECTION_NAME).add_children( + XBlockFixtureDesc('vertical', self.UNIT_NAME).add_children( + create_multiple_choice_problem(self.PROBLEM_NAME) + ) ) ) ).install() @@ -953,15 +962,6 @@ class ProgressPageTest(UniqueCourseTest): # Auto-auth register for the course. _auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - def test_progress_page_shows_scored_problems(self): - with self._logged_in_session(): - self.assertEqual(self._get_scores(), [(0, 1)]) - self.assertEqual(self._get_section_score(), (0, 1)) - self.courseware_page.visit() - self._answer_problem_correctly() - self.assertEqual(self._get_scores(), [(1, 1)]) - self.assertEqual(self._get_section_score(), (1, 1)) - def _answer_problem_correctly(self): """ Submit a correct answer to the problem. @@ -975,24 +975,156 @@ class ProgressPageTest(UniqueCourseTest): Return a list of scores from the progress page. """ self.progress_page.visit() - return self.progress_page.section_score('Test Section 1', 'Test Subsection 1') + return self.progress_page.section_score(self.SECTION_NAME, self.SUBSECTION_NAME) def _get_scores(self): """ Return a list of scores from the progress page. """ self.progress_page.visit() - return self.progress_page.scores('Test Section 1', 'Test Subsection 1') + return self.progress_page.scores(self.SECTION_NAME, self.SUBSECTION_NAME) @contextmanager - def _logged_in_session(self): + def _logged_in_session(self, staff=False): """ Ensure that the user is logged in and out appropriately at the beginning and end of the current test. """ self.logout_page.visit() try: - _auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) + if staff: + _auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) + else: + _auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) yield finally: self.logout_page.visit() + + +class ProgressPageTest(ProgressPageBaseTest): + """ + Test that the progress page reports scores from completed assessments. + """ + def test_progress_page_shows_scored_problems(self): + with self._logged_in_session(): + self.assertEqual(self._get_scores(), [(0, 1)]) + self.assertEqual(self._get_section_score(), (0, 1)) + self.courseware_page.visit() + self._answer_problem_correctly() + self.assertEqual(self._get_scores(), [(1, 1)]) + self.assertEqual(self._get_section_score(), (1, 1)) + + +@ddt.ddt +class PersistentGradesTest(ProgressPageBaseTest): + """ + Test that grades for completed assessments are persisted + when various edits are made. + """ + def setUp(self): + super(PersistentGradesTest, self).setUp() + self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) + + def _change_subsection_structure(self): + """ + Adds a unit to the subsection, which + should not affect a persisted subsectiong grade. + """ + with self._logged_in_session(staff=True): + self.course_outline.visit() + subsection = self.course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME) + subsection.expand_subsection() + subsection.add_unit() + + def _set_staff_lock_on_subsection(self, locked): + """ + Sets staff lock for a subsection, which should hide the + subsection score from students on the progress page. + """ + with self._logged_in_session(staff=True): + self.course_outline.visit() + subsection = self.course_outline.section_at(0).subsection_at(0) + subsection.set_staff_lock(locked) + self.assertEqual(subsection.has_staff_lock_warning, locked) + + def _change_weight_for_problem(self): + """ + Changes the weight of the problem, which should not affect + persisted grades. + """ + with self._logged_in_session(staff=True): + self.course_outline.visit() + self.course_outline.section_at(0).subsection_at(0).expand_subsection() + unit = self.course_outline.section_at(0).subsection_at(0).unit(self.UNIT_NAME).go_to() + container = unit.xblocks[0].go_to_container() + component = container.xblocks[0].children[0] + + component.edit() + component_editor = ComponentEditorView(self.browser, component.locator) + component_editor.set_field_value_and_save('Problem Weight', 5) + + def _rescore_for_all(self): + """ + Triggers a rescore for all students. Currently unimplemented. + """ + pass + + def _edit_problem_content(self): + """ + Replaces the content of a problem with other html. + Should not affect persisted grades. + """ + with self._logged_in_session(staff=True): + self.course_outline.visit() + self.course_outline.section_at(0).subsection_at(0).expand_subsection() + unit = self.course_outline.section_at(0).subsection_at(0).unit(self.UNIT_NAME).go_to() + component = unit.xblocks[1] + edit_view = component.edit() + + modified_content = "

modified content

" + # Set content in the CodeMirror editor. + type_in_codemirror(self, 0, modified_content) + + edit_view.q(css='.action-save').click() + + @ddt.data( + _edit_problem_content, + _change_subsection_structure, + _change_weight_for_problem, + _rescore_for_all + ) + def test_content_changes_do_not_change_score(self, edit): + with self._logged_in_session(): + self.assertEqual(self._get_scores(), [(0, 1)]) + self.assertEqual(self._get_section_score(), (0, 1)) + self.courseware_page.visit() + self._answer_problem_correctly() + self.assertEqual(self._get_scores(), [(1, 1)]) + self.assertEqual(self._get_section_score(), (1, 1)) + + edit(self) + + with self._logged_in_session(): + self.assertEqual(self._get_scores(), [(1, 1)]) + self.assertEqual(self._get_section_score(), (1, 1)) + + def test_visibility_change_does_affect_score(self): + with self._logged_in_session(): + self.assertEqual(self._get_scores(), [(0, 1)]) + self.assertEqual(self._get_section_score(), (0, 1)) + self.courseware_page.visit() + self._answer_problem_correctly() + self.assertEqual(self._get_scores(), [(1, 1)]) + self.assertEqual(self._get_section_score(), (1, 1)) + + self._set_staff_lock_on_subsection(True) + + with self._logged_in_session(): + self.assertEqual(self._get_scores(), None) + self.assertEqual(self._get_section_score(), None) + + self._set_staff_lock_on_subsection(False) + + with self._logged_in_session(): + self.assertEqual(self._get_scores(), [(1, 1)]) + self.assertEqual(self._get_section_score(), (1, 1))