Support for rescoring a problem only if the new score is higher
TNL-5046
This commit is contained in:
@@ -1048,7 +1048,7 @@ class CapaMixin(CapaFields):
|
||||
|
||||
return answers
|
||||
|
||||
def publish_grade(self):
|
||||
def publish_grade(self, only_if_higher=None):
|
||||
"""
|
||||
Publishes the student's current grade to the system as an event
|
||||
"""
|
||||
@@ -1059,6 +1059,7 @@ class CapaMixin(CapaFields):
|
||||
{
|
||||
'value': score['score'],
|
||||
'max_value': score['total'],
|
||||
'only_if_higher': only_if_higher,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1370,13 +1371,16 @@ class CapaMixin(CapaFields):
|
||||
|
||||
return input_metadata
|
||||
|
||||
def rescore_problem(self):
|
||||
def rescore_problem(self, only_if_higher):
|
||||
"""
|
||||
Checks whether the existing answers to a problem are correct.
|
||||
|
||||
This is called when the correct answer to a problem has been changed,
|
||||
and the grade should be re-evaluated.
|
||||
|
||||
If only_if_higher is True, the answer and grade are updated
|
||||
only if the resulting score is higher than before.
|
||||
|
||||
Returns a dict with one key:
|
||||
{'success' : 'correct' | 'incorrect' | AJAX alert msg string }
|
||||
|
||||
@@ -1428,7 +1432,7 @@ class CapaMixin(CapaFields):
|
||||
# need to increment here, or mark done. Just save.
|
||||
self.set_state_from_lcp()
|
||||
|
||||
self.publish_grade()
|
||||
self.publish_grade(only_if_higher)
|
||||
|
||||
new_score = self.lcp.get_score()
|
||||
event_info['new_score'] = new_score['score']
|
||||
|
||||
@@ -487,15 +487,14 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Simulate that all answers are marked correct, no matter
|
||||
# what the input is, by patching CorrectMap.is_correct()
|
||||
# Also simulate rendering the HTML
|
||||
# TODO: pep8 thinks the following line has invalid syntax
|
||||
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct, \
|
||||
patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
|
||||
mock_is_correct.return_value = True
|
||||
mock_html.return_value = "Test HTML"
|
||||
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
|
||||
with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
|
||||
mock_is_correct.return_value = True
|
||||
mock_html.return_value = "Test HTML"
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.submit_problem(get_request_dict)
|
||||
# Check the problem
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.submit_problem(get_request_dict)
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
@@ -861,7 +860,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# what the input is, by patching LoncapaResponse.evaluate_answers()
|
||||
with patch('capa.responsetypes.LoncapaResponse.evaluate_answers') as mock_evaluate_answers:
|
||||
mock_evaluate_answers.return_value = CorrectMap(CapaFactory.answer_key(), 'correct')
|
||||
result = module.rescore_problem()
|
||||
result = module.rescore_problem(only_if_higher=False)
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
@@ -881,7 +880,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# what the input is, by patching LoncapaResponse.evaluate_answers()
|
||||
with patch('capa.responsetypes.LoncapaResponse.evaluate_answers') as mock_evaluate_answers:
|
||||
mock_evaluate_answers.return_value = CorrectMap(CapaFactory.answer_key(), 'incorrect')
|
||||
result = module.rescore_problem()
|
||||
result = module.rescore_problem(only_if_higher=False)
|
||||
|
||||
# Expect that the problem is marked incorrect
|
||||
self.assertEqual(result['success'], 'incorrect')
|
||||
@@ -895,7 +894,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
# Try to rescore the problem, and get exception
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
module.rescore_problem()
|
||||
module.rescore_problem(only_if_higher=False)
|
||||
|
||||
def test_rescore_problem_not_supported(self):
|
||||
module = CapaFactory.create(done=True)
|
||||
@@ -904,7 +903,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
with patch('capa.capa_problem.LoncapaProblem.supports_rescoring') as mock_supports_rescoring:
|
||||
mock_supports_rescoring.return_value = False
|
||||
with self.assertRaises(NotImplementedError):
|
||||
module.rescore_problem()
|
||||
module.rescore_problem(only_if_higher=False)
|
||||
|
||||
def _rescore_problem_error_helper(self, exception_class):
|
||||
"""Helper to allow testing all errors that rescoring might return."""
|
||||
@@ -914,7 +913,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Simulate answering a problem that raises the exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.rescore_existing_answers') as mock_rescore:
|
||||
mock_rescore.side_effect = exception_class(u'test error \u03a9')
|
||||
result = module.rescore_problem()
|
||||
result = module.rescore_problem(only_if_higher=False)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
expected_msg = u'Error: test error \u03a9'
|
||||
@@ -1656,7 +1655,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module.submit_problem(get_request_dict)
|
||||
# On rescore, state/student_answers should use unmasked names
|
||||
with patch.object(module.runtime, 'track_function') as mock_track_function:
|
||||
module.rescore_problem()
|
||||
module.rescore_problem(only_if_higher=False)
|
||||
mock_call = mock_track_function.mock_calls[0]
|
||||
event_info = mock_call[1][1]
|
||||
self.assertEquals(mock_call[1][0], 'problem_rescore')
|
||||
|
||||
@@ -48,12 +48,14 @@ class InstructorDashboardPage(CoursePage):
|
||||
data_download_section.wait_for_page()
|
||||
return data_download_section
|
||||
|
||||
def select_student_admin(self):
|
||||
def select_student_admin(self, admin_class):
|
||||
"""
|
||||
Selects the student admin tab and returns the MembershipSection
|
||||
Selects the student admin tab and returns the requested
|
||||
admin section.
|
||||
admin_class should be a subclass of StudentAdminPage.
|
||||
"""
|
||||
self.q(css='[data-section="student_admin"]').first.click()
|
||||
student_admin_section = StudentAdminPage(self.browser)
|
||||
student_admin_section = admin_class(self.browser)
|
||||
student_admin_section.wait_for_page()
|
||||
return student_admin_section
|
||||
|
||||
@@ -1025,8 +1027,18 @@ class StudentAdminPage(PageObject):
|
||||
Student admin section of the Instructor dashboard.
|
||||
"""
|
||||
url = None
|
||||
EE_CONTAINER = ".entrance-exam-grade-container"
|
||||
CS_CONTAINER = ".course-specific-container"
|
||||
CONTAINER = None
|
||||
|
||||
PROBLEM_INPUT_NAME = None
|
||||
STUDENT_EMAIL_INPUT_NAME = None
|
||||
|
||||
RESET_ATTEMPTS_BUTTON_NAME = None
|
||||
RESCORE_BUTTON_NAME = None
|
||||
RESCORE_IF_HIGHER_BUTTON_NAME = None
|
||||
DELETE_STATE_BUTTON_NAME = None
|
||||
|
||||
BACKGROUND_TASKS_BUTTON_NAME = None
|
||||
TASK_HISTORY_TABLE_NAME = None
|
||||
|
||||
def is_browser_on_page(self):
|
||||
"""
|
||||
@@ -1034,167 +1046,185 @@ class StudentAdminPage(PageObject):
|
||||
"""
|
||||
return self.q(css='[data-section=student_admin].active-section').present
|
||||
|
||||
def _input_with_name(self, input_name):
|
||||
"""
|
||||
Returns the input box with the given name
|
||||
for this object's container.
|
||||
"""
|
||||
return self.q(css='{} input[name={}]'.format(self.CONTAINER, input_name))
|
||||
|
||||
@property
|
||||
def student_email_input(self):
|
||||
def problem_location_input(self):
|
||||
"""
|
||||
Returns input box for problem location
|
||||
"""
|
||||
return self._input_with_name(self.PROBLEM_INPUT_NAME)
|
||||
|
||||
def set_problem_location(self, problem_location):
|
||||
"""
|
||||
Returns input box for problem location
|
||||
"""
|
||||
input_box = self.problem_location_input.first.results[0]
|
||||
input_box.send_keys(unicode(problem_location))
|
||||
|
||||
@property
|
||||
def student_email_or_username_input(self):
|
||||
"""
|
||||
Returns email address/username input box.
|
||||
"""
|
||||
return self.q(css='{} input[name=entrance-exam-student-select-grade]'.format(self.EE_CONTAINER))
|
||||
return self._input_with_name(self.STUDENT_EMAIL_INPUT_NAME)
|
||||
|
||||
@property
|
||||
def rescore_problem_input(self):
|
||||
def set_student_email_or_username(self, email_or_username):
|
||||
"""
|
||||
Returns input box for rescore/reset all on a problem
|
||||
Sets given email or username as value of
|
||||
student email/username input box.
|
||||
"""
|
||||
return self.q(css='{} input[name=problem-select-all]'.format(self.CS_CONTAINER))
|
||||
input_box = self.student_email_or_username_input.first.results[0]
|
||||
input_box.send_keys(email_or_username)
|
||||
|
||||
@property
|
||||
def reset_attempts_button(self):
|
||||
"""
|
||||
Returns reset student attempts button.
|
||||
"""
|
||||
return self.q(css='{} input[name=reset-entrance-exam-attempts]'.format(self.EE_CONTAINER))
|
||||
return self._input_with_name(self.RESET_ATTEMPTS_BUTTON_NAME)
|
||||
|
||||
@property
|
||||
def rescore_submission_button(self):
|
||||
def rescore_button(self):
|
||||
"""
|
||||
Returns rescore student submission button.
|
||||
Returns rescore button.
|
||||
"""
|
||||
return self.q(css='{} input[name=rescore-entrance-exam]'.format(self.EE_CONTAINER))
|
||||
return self._input_with_name(self.RESCORE_BUTTON_NAME)
|
||||
|
||||
@property
|
||||
def rescore_all_submissions_button(self):
|
||||
def rescore_if_higher_button(self):
|
||||
"""
|
||||
Returns rescore student submission button.
|
||||
Returns rescore if higher button.
|
||||
"""
|
||||
return self.q(css='{} input[name=rescore-problem-all]'.format(self.CS_CONTAINER))
|
||||
return self._input_with_name(self.RESCORE_IF_HIGHER_BUTTON_NAME)
|
||||
|
||||
@property
|
||||
def show_background_tasks_button(self):
|
||||
def delete_state_button(self):
|
||||
"""
|
||||
Return Show Background Tasks button.
|
||||
Returns delete state button.
|
||||
"""
|
||||
return self.q(css='{} input[name=task-history-all]'.format(self.CS_CONTAINER))
|
||||
return self._input_with_name(self.DELETE_STATE_BUTTON_NAME)
|
||||
|
||||
@property
|
||||
def task_history_button(self):
|
||||
"""
|
||||
Return Background Tasks History button.
|
||||
"""
|
||||
return self._input_with_name(self.BACKGROUND_TASKS_BUTTON_NAME)
|
||||
|
||||
def wait_for_task_history_table(self):
|
||||
"""
|
||||
Waits until the task history table is visible.
|
||||
"""
|
||||
def check_func():
|
||||
"""
|
||||
Promise Check Function
|
||||
"""
|
||||
query = self.q(css="{} .{}".format(self.CONTAINER, self.TASK_HISTORY_TABLE_NAME))
|
||||
return query.visible, query
|
||||
|
||||
return Promise(check_func, "Waiting for student admin task history table to be visible.").fulfill()
|
||||
|
||||
def wait_for_task_completion(self, expected_task_string):
|
||||
"""
|
||||
Waits until the task history table is visible.
|
||||
"""
|
||||
def check_func():
|
||||
"""
|
||||
Promise Check Function
|
||||
"""
|
||||
self.task_history_button.click()
|
||||
table = self.wait_for_task_history_table()
|
||||
return len(table) > 0 and expected_task_string in table.results[0].text
|
||||
|
||||
return EmptyPromise(check_func, "Waiting for student admin task to complete.").fulfill()
|
||||
|
||||
|
||||
class StudentSpecificAdmin(StudentAdminPage):
|
||||
"""
|
||||
Student specific section of the Student Admin page.
|
||||
"""
|
||||
CONTAINER = ".student-grade-container"
|
||||
|
||||
PROBLEM_INPUT_NAME = "problem-select-single"
|
||||
STUDENT_EMAIL_INPUT_NAME = "student-select-grade"
|
||||
|
||||
RESET_ATTEMPTS_BUTTON_NAME = "reset-attempts-single"
|
||||
RESCORE_BUTTON_NAME = "rescore-problem-single"
|
||||
RESCORE_IF_HIGHER_BUTTON_NAME = "rescore-problem-if-higher-single"
|
||||
DELETE_STATE_BUTTON_NAME = "delete-state-single"
|
||||
|
||||
BACKGROUND_TASKS_BUTTON_NAME = "task-history-single"
|
||||
TASK_HISTORY_TABLE_NAME = "task-history-single-table"
|
||||
|
||||
|
||||
class CourseSpecificAdmin(StudentAdminPage):
|
||||
"""
|
||||
Course specific section of the Student Admin page.
|
||||
"""
|
||||
CONTAINER = ".course-specific-container"
|
||||
|
||||
PROBLEM_INPUT_NAME = "problem-select-all"
|
||||
STUDENT_EMAIL_INPUT_NAME = None
|
||||
|
||||
RESET_ATTEMPTS_BUTTON_NAME = "reset-attempts-all"
|
||||
RESCORE_BUTTON_NAME = "rescore-problem-all"
|
||||
RESCORE_IF_HIGHER_BUTTON_NAME = "rescore-problem-all-if-higher"
|
||||
DELETE_STATE_BUTTON_NAME = None
|
||||
|
||||
BACKGROUND_TASKS_BUTTON_NAME = "task-history-all"
|
||||
TASK_HISTORY_TABLE_NAME = "task-history-all-table"
|
||||
|
||||
|
||||
class EntranceExamAdmin(StudentAdminPage):
|
||||
"""
|
||||
Entrance exam section of the Student Admin page.
|
||||
"""
|
||||
CONTAINER = ".entrance-exam-grade-container"
|
||||
|
||||
STUDENT_EMAIL_INPUT_NAME = "entrance-exam-student-select-grade"
|
||||
PROBLEM_INPUT_NAME = None
|
||||
|
||||
RESET_ATTEMPTS_BUTTON_NAME = "reset-entrance-exam-attempts"
|
||||
RESCORE_BUTTON_NAME = "rescore-entrance-exam"
|
||||
RESCORE_IF_HIGHER_BUTTON_NAME = "rescore-entrance-exam-if-higher"
|
||||
DELETE_STATE_BUTTON_NAME = "delete-entrance-exam-state"
|
||||
|
||||
BACKGROUND_TASKS_BUTTON_NAME = "entrance-exam-task-history"
|
||||
TASK_HISTORY_TABLE_NAME = "entrance-exam-task-history-table"
|
||||
|
||||
@property
|
||||
def skip_entrance_exam_button(self):
|
||||
"""
|
||||
Return Let Student Skip Entrance Exam button.
|
||||
"""
|
||||
return self.q(css='{} input[name=skip-entrance-exam]'.format(self.EE_CONTAINER))
|
||||
|
||||
@property
|
||||
def delete_student_state_button(self):
|
||||
"""
|
||||
Returns delete student state button.
|
||||
"""
|
||||
return self.q(css='{} input[name=delete-entrance-exam-state]'.format(self.EE_CONTAINER))
|
||||
|
||||
@property
|
||||
def background_task_history_button(self):
|
||||
"""
|
||||
Returns show background task history for student button.
|
||||
"""
|
||||
return self.q(css='{} input[name=entrance-exam-task-history]'.format(self.EE_CONTAINER))
|
||||
return self.q(css='{} input[name=skip-entrance-exam]'.format(self.CONTAINER))
|
||||
|
||||
@property
|
||||
def top_notification(self):
|
||||
"""
|
||||
Returns show background task history for student button.
|
||||
"""
|
||||
return self.q(css='{} .request-response-error'.format(self.EE_CONTAINER)).first
|
||||
return self.q(css='{} .request-response-error'.format(self.CONTAINER)).first
|
||||
|
||||
def is_student_email_input_visible(self):
|
||||
def are_all_buttons_visible(self):
|
||||
"""
|
||||
Returns True if student email address/username input box is present.
|
||||
Returns whether all buttons related to entrance exams
|
||||
are visible.
|
||||
"""
|
||||
return self.student_email_input.is_present()
|
||||
|
||||
def is_reset_attempts_button_visible(self):
|
||||
"""
|
||||
Returns True if reset student attempts button is present.
|
||||
"""
|
||||
return self.reset_attempts_button.is_present()
|
||||
|
||||
def is_rescore_submission_button_visible(self):
|
||||
"""
|
||||
Returns True if rescore student submission button is present.
|
||||
"""
|
||||
return self.rescore_submission_button.is_present()
|
||||
|
||||
def is_delete_student_state_button_visible(self):
|
||||
"""
|
||||
Returns True if delete student state for entrance exam button is present.
|
||||
"""
|
||||
return self.delete_student_state_button.is_present()
|
||||
|
||||
def is_background_task_history_button_visible(self):
|
||||
"""
|
||||
Returns True if show background task history for student button is present.
|
||||
"""
|
||||
return self.background_task_history_button.is_present()
|
||||
|
||||
def is_background_task_history_table_visible(self):
|
||||
"""
|
||||
Returns True if background task history table is present.
|
||||
"""
|
||||
return self.q(css='{} .entrance-exam-task-history-table'.format(self.EE_CONTAINER)).is_present()
|
||||
|
||||
def click_reset_attempts_button(self):
|
||||
"""
|
||||
clicks reset student attempts button.
|
||||
"""
|
||||
return self.reset_attempts_button.click()
|
||||
|
||||
def click_rescore_submissions_button(self):
|
||||
"""
|
||||
clicks rescore submissions button.
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return self.skip_entrance_exam_button.click()
|
||||
|
||||
def click_delete_student_state_button(self):
|
||||
"""
|
||||
clicks delete student state button.
|
||||
"""
|
||||
return self.delete_student_state_button.click()
|
||||
|
||||
def click_task_history_button(self):
|
||||
"""
|
||||
clicks background task history button.
|
||||
"""
|
||||
return self.background_task_history_button.click()
|
||||
|
||||
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_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)
|
||||
return (
|
||||
self.student_email_or_username_input.is_present() and
|
||||
self.reset_attempts_button.is_present() and
|
||||
self.rescore_button.is_present() and
|
||||
self.rescore_if_higher_button.is_present() and
|
||||
self.delete_state_button.is_present() and
|
||||
self.task_history_button.is_present()
|
||||
)
|
||||
|
||||
|
||||
class CertificatesPage(PageObject):
|
||||
|
||||
@@ -107,6 +107,15 @@ class StaffDebugPage(PageObject):
|
||||
self.q(css='input[id^=sd_fu_]').first.fill(user)
|
||||
self.q(css='.staff-modal .staff-debug-rescore').click()
|
||||
|
||||
def rescore_if_higher(self, user=None):
|
||||
"""
|
||||
This clicks on the reset attempts link with an optionally
|
||||
specified user.
|
||||
"""
|
||||
if user:
|
||||
self.q(css='input[id^=sd_fu_]').first.fill(user)
|
||||
self.q(css='.staff-modal .staff-debug-rescore-if-higher').click()
|
||||
|
||||
@property
|
||||
def idash_msg(self):
|
||||
"""
|
||||
|
||||
@@ -169,6 +169,13 @@ class ContainerPage(PageObject, HelpMixin):
|
||||
"""
|
||||
return self.q(css='.action-publish').first
|
||||
|
||||
def publish(self):
|
||||
"""
|
||||
Publishes the container.
|
||||
"""
|
||||
self.publish_action.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def discard_changes(self):
|
||||
"""
|
||||
Discards draft changes (which will then re-render the page).
|
||||
|
||||
@@ -353,17 +353,29 @@ def is_404_page(browser):
|
||||
return 'Page not found (404)' in browser.find_element_by_tag_name('h1').text
|
||||
|
||||
|
||||
def create_multiple_choice_xml(correct_choice=2, num_choices=4):
|
||||
"""
|
||||
Return the Multiple Choice Problem XML, given the name of the problem.
|
||||
"""
|
||||
# all choices are incorrect except for correct_choice
|
||||
choices = [False for _ in range(num_choices)]
|
||||
choices[correct_choice] = True
|
||||
|
||||
choice_names = ['choice_{}'.format(index) for index in range(num_choices)]
|
||||
question_text = 'The correct answer is Choice {}'.format(correct_choice)
|
||||
|
||||
return MultipleChoiceResponseXMLFactory().build_xml(
|
||||
question_text=question_text,
|
||||
choices=choices,
|
||||
choice_names=choice_names,
|
||||
)
|
||||
|
||||
|
||||
def create_multiple_choice_problem(problem_name):
|
||||
"""
|
||||
Return the Multiple Choice Problem Descriptor, given the name of the problem.
|
||||
"""
|
||||
factory = MultipleChoiceResponseXMLFactory()
|
||||
xml_data = factory.build_xml(
|
||||
question_text='The correct answer is Choice 2',
|
||||
choices=[False, False, True, False],
|
||||
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
|
||||
)
|
||||
|
||||
xml_data = create_multiple_choice_xml()
|
||||
return XBlockFixtureDesc(
|
||||
'problem',
|
||||
problem_name,
|
||||
|
||||
@@ -14,7 +14,7 @@ from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
|
||||
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage
|
||||
from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage, EntranceExamAdmin
|
||||
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from common.test.acceptance.pages.lms.dashboard import DashboardPage
|
||||
from common.test.acceptance.pages.lms.problem import ProblemPage
|
||||
@@ -403,10 +403,17 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
|
||||
|
||||
|
||||
@attr(shard=7)
|
||||
@ddt.ddt
|
||||
class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
"""
|
||||
Tests for Entrance exam specific student grading tasks.
|
||||
"""
|
||||
admin_buttons = (
|
||||
'reset_attempts_button',
|
||||
'rescore_button',
|
||||
'rescore_if_higher_button',
|
||||
'delete_state_button',
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(EntranceExamGradeTest, self).setUp()
|
||||
@@ -426,7 +433,7 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
|
||||
# go to the student admin page on the instructor dashboard
|
||||
self.log_in_as_instructor()
|
||||
self.student_admin_section = self.visit_instructor_dashboard().select_student_admin()
|
||||
self.entrance_exam_admin = self.visit_instructor_dashboard().select_student_admin(EntranceExamAdmin)
|
||||
|
||||
def test_input_text_and_buttons_are_visible(self):
|
||||
"""
|
||||
@@ -437,86 +444,57 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
Then I see Student Email input box, Reset Student Attempt, Rescore Student Submission,
|
||||
Delete Student State for entrance exam and Show Background Task History for Student buttons
|
||||
"""
|
||||
self.assertTrue(self.student_admin_section.is_student_email_input_visible())
|
||||
self.assertTrue(self.student_admin_section.is_reset_attempts_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_rescore_submission_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_delete_student_state_button_visible())
|
||||
self.assertTrue(self.student_admin_section.is_background_task_history_button_visible())
|
||||
self.assertTrue(self.entrance_exam_admin.are_all_buttons_visible())
|
||||
|
||||
def test_clicking_reset_student_attempts_button_without_email_shows_error(self):
|
||||
@ddt.data(*admin_buttons)
|
||||
def test_admin_button_without_email_shows_error(self, button_to_test):
|
||||
"""
|
||||
Scenario: Clicking on the Reset Student Attempts button without entering student email
|
||||
Scenario: Clicking on the requested button without entering student email
|
||||
address or username results in error.
|
||||
Given that I am on the Student Admin tab on the Instructor Dashboard
|
||||
When I click the Reset Student Attempts Button under Entrance Exam Grade
|
||||
When I click the requested button under Entrance Exam Grade
|
||||
Adjustment without enter an email address
|
||||
Then I should be shown an Error Notification
|
||||
And The Notification message should read 'Please enter a student email address or username.'
|
||||
"""
|
||||
self.student_admin_section.click_reset_attempts_button()
|
||||
getattr(self.entrance_exam_admin, button_to_test).click()
|
||||
self.assertEqual(
|
||||
'Please enter a student email address or username.',
|
||||
self.student_admin_section.top_notification.text[0]
|
||||
self.entrance_exam_admin.top_notification.text[0]
|
||||
)
|
||||
|
||||
def test_clicking_reset_student_attempts_button_with_success(self):
|
||||
@ddt.data(*admin_buttons)
|
||||
def test_admin_button_with_success(self, button_to_test):
|
||||
"""
|
||||
Scenario: Clicking on the Reset Student Attempts button with valid student email
|
||||
Scenario: Clicking on the requested button with valid student email
|
||||
address or username should result in success prompt.
|
||||
Given that I am on the Student Admin tab on the Instructor Dashboard
|
||||
When I click the Reset Student Attempts Button under Entrance Exam Grade
|
||||
When I click the requested button under Entrance Exam Grade
|
||||
Adjustment after entering a valid student
|
||||
email address or username
|
||||
Then I should be shown an alert with success message
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_reset_attempts_button()
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
self.entrance_exam_admin.set_student_email_or_username(self.student_identifier)
|
||||
getattr(self.entrance_exam_admin, button_to_test).click()
|
||||
alert = get_modal_alert(self.entrance_exam_admin.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_clicking_reset_student_attempts_button_with_error(self):
|
||||
@ddt.data(*admin_buttons)
|
||||
def test_admin_button_with_error(self, button_to_test):
|
||||
"""
|
||||
Scenario: Clicking on the Reset Student Attempts button with email address or username
|
||||
Scenario: Clicking on the requested button with email address or username
|
||||
of a non existing student should result in error message.
|
||||
Given that I am on the Student Admin tab on the Instructor Dashboard
|
||||
When I click the Reset Student Attempts Button under Entrance Exam Grade
|
||||
When I click the requested Button under Entrance Exam Grade
|
||||
Adjustment after non existing student email address or username
|
||||
Then I should be shown an error message
|
||||
"""
|
||||
self.student_admin_section.set_student_email('non_existing@example.com')
|
||||
self.student_admin_section.click_reset_attempts_button()
|
||||
self.student_admin_section.wait_for_ajax()
|
||||
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
|
||||
self.entrance_exam_admin.set_student_email_or_username('non_existing@example.com')
|
||||
getattr(self.entrance_exam_admin, button_to_test).click()
|
||||
self.entrance_exam_admin.wait_for_ajax()
|
||||
self.assertGreater(len(self.entrance_exam_admin.top_notification.text[0]), 0)
|
||||
|
||||
def test_clicking_rescore_submission_button_with_success(self):
|
||||
"""
|
||||
Scenario: Clicking on the Rescore Student Submission button with valid student email
|
||||
address or username should result in success prompt.
|
||||
Given that I am on the Student Admin tab on the Instructor Dashboard
|
||||
When I click the Rescore Student Submission Button under Entrance Exam Grade
|
||||
Adjustment after entering a valid student email address or username
|
||||
Then I should be shown an alert with success message
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_rescore_submissions_button()
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_clicking_rescore_submission_button_with_error(self):
|
||||
"""
|
||||
Scenario: Clicking on the Rescore Student Submission button with email address or username
|
||||
of a non existing student should result in error message.
|
||||
Given that I am on the Student Admin tab on the Instructor Dashboard
|
||||
When I click the Rescore Student Submission Button under Entrance Exam Grade
|
||||
Adjustment after non existing student email address or username
|
||||
Then I should be shown an error message
|
||||
"""
|
||||
self.student_admin_section.set_student_email('non_existing@example.com')
|
||||
self.student_admin_section.click_rescore_submissions_button()
|
||||
self.student_admin_section.wait_for_ajax()
|
||||
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
|
||||
|
||||
def test_clicking_skip_entrance_exam_button_with_success(self):
|
||||
def test_skip_entrance_exam_button_with_success(self):
|
||||
"""
|
||||
Scenario: Clicking on the Let Student Skip Entrance Exam button with
|
||||
valid student email address or username should result in success prompt.
|
||||
@@ -526,17 +504,18 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
email address or username
|
||||
Then I should be shown an alert with success message
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_skip_entrance_exam_button()
|
||||
self.entrance_exam_admin.set_student_email_or_username(self.student_identifier)
|
||||
self.entrance_exam_admin.skip_entrance_exam_button.click()
|
||||
|
||||
#first we have window.confirm
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert = get_modal_alert(self.entrance_exam_admin.browser)
|
||||
alert.accept()
|
||||
|
||||
# then we have alert confirming action
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert = get_modal_alert(self.entrance_exam_admin.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_clicking_skip_entrance_exam_button_with_error(self):
|
||||
def test_skip_entrance_exam_button_with_error(self):
|
||||
"""
|
||||
Scenario: Clicking on the Let Student Skip Entrance Exam button with
|
||||
email address or username of a non existing student should result in error message.
|
||||
@@ -546,47 +525,17 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
student email address or username
|
||||
Then I should be shown an error message
|
||||
"""
|
||||
self.student_admin_section.set_student_email('non_existing@example.com')
|
||||
self.student_admin_section.click_skip_entrance_exam_button()
|
||||
self.entrance_exam_admin.set_student_email_or_username('non_existing@example.com')
|
||||
self.entrance_exam_admin.skip_entrance_exam_button.click()
|
||||
|
||||
#first we have window.confirm
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert = get_modal_alert(self.entrance_exam_admin.browser)
|
||||
alert.accept()
|
||||
|
||||
self.student_admin_section.wait_for_ajax()
|
||||
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
|
||||
self.entrance_exam_admin.wait_for_ajax()
|
||||
self.assertGreater(len(self.entrance_exam_admin.top_notification.text[0]), 0)
|
||||
|
||||
def test_clicking_delete_student_attempts_button_with_success(self):
|
||||
"""
|
||||
Scenario: Clicking on the Delete Student State for entrance exam button
|
||||
with valid student email address or username should result in success prompt.
|
||||
Given that I am on the Student Admin tab on the Instructor Dashboard
|
||||
When I click the Delete Student State for entrance exam Button
|
||||
under Entrance Exam Grade Adjustment after entering a valid student
|
||||
email address or username
|
||||
Then I should be shown an alert with success message
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_delete_student_state_button()
|
||||
alert = get_modal_alert(self.student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
|
||||
def test_clicking_delete_student_attempts_button_with_error(self):
|
||||
"""
|
||||
Scenario: Clicking on the Delete Student State for entrance exam button
|
||||
with email address or username of a non existing student should result
|
||||
in error message.
|
||||
Given that I am on the Student Admin tab on the Instructor Dashboard
|
||||
When I click the Delete Student State for entrance exam Button
|
||||
under Entrance Exam Grade Adjustment after non existing student
|
||||
email address or username
|
||||
Then I should be shown an error message
|
||||
"""
|
||||
self.student_admin_section.set_student_email('non_existing@example.com')
|
||||
self.student_admin_section.click_delete_student_state_button()
|
||||
self.student_admin_section.wait_for_ajax()
|
||||
self.assertGreater(len(self.student_admin_section.top_notification.text[0]), 0)
|
||||
|
||||
def test_clicking_task_history_button_with_success(self):
|
||||
def test_task_history_button_with_success(self):
|
||||
"""
|
||||
Scenario: Clicking on the Show Background Task History for Student
|
||||
with valid student email address or username should result in table of tasks.
|
||||
@@ -594,11 +543,11 @@ class EntranceExamGradeTest(BaseInstructorDashboardTest):
|
||||
When I click the Show Background Task History for Student Button
|
||||
under Entrance Exam Grade Adjustment after entering a valid student
|
||||
email address or username
|
||||
Then I should be shown an table listing all background tasks
|
||||
Then I should be shown a table listing all background tasks
|
||||
"""
|
||||
self.student_admin_section.set_student_email(self.student_identifier)
|
||||
self.student_admin_section.click_task_history_button()
|
||||
self.assertTrue(self.student_admin_section.is_background_task_history_table_visible())
|
||||
self.entrance_exam_admin.set_student_email_or_username(self.student_identifier)
|
||||
self.entrance_exam_admin.task_history_button.click()
|
||||
self.entrance_exam_admin.wait_for_task_history_table()
|
||||
|
||||
|
||||
@attr(shard=7)
|
||||
|
||||
@@ -116,8 +116,9 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = self._goto_staff_page().open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
self.assertEqual(
|
||||
u'Successfully reset the attempts for user {}'.format(self.USERNAME), msg,
|
||||
)
|
||||
|
||||
def test_delete_state_empty(self):
|
||||
"""
|
||||
@@ -126,8 +127,9 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = self._goto_staff_page().open_staff_debug_info()
|
||||
staff_debug_page.delete_state()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
self.assertEqual(
|
||||
u'Successfully deleted student state for user {}'.format(self.USERNAME), msg,
|
||||
)
|
||||
|
||||
def test_reset_attempts_state(self):
|
||||
"""
|
||||
@@ -139,10 +141,11 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
self.assertEqual(
|
||||
u'Successfully reset the attempts for user {}'.format(self.USERNAME), msg,
|
||||
)
|
||||
|
||||
def test_rescore_state(self):
|
||||
def test_rescore_problem(self):
|
||||
"""
|
||||
Rescore the student
|
||||
"""
|
||||
@@ -152,7 +155,19 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.rescore()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully rescored problem for user STAFF_TESTER', msg)
|
||||
self.assertEqual(u'Successfully rescored problem for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_rescore_problem_if_higher(self):
|
||||
"""
|
||||
Rescore the student
|
||||
"""
|
||||
staff_page = self._goto_staff_page()
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.rescore_if_higher()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully rescored problem to improve score for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_student_state_delete(self):
|
||||
"""
|
||||
@@ -164,8 +179,7 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.delete_state()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
self.assertEqual(u'Successfully deleted student state for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_student_by_email(self):
|
||||
"""
|
||||
@@ -177,8 +191,7 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts(self.EMAIL)
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.EMAIL), msg)
|
||||
self.assertEqual(u'Successfully reset the attempts for user {}'.format(self.EMAIL), msg)
|
||||
|
||||
def test_bad_student(self):
|
||||
"""
|
||||
@@ -189,8 +202,7 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.delete_state('INVALIDUSER')
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Failed to delete student state. '
|
||||
'User does not exist.', msg)
|
||||
self.assertEqual(u'Failed to delete student state for user. User does not exist.', msg)
|
||||
|
||||
def test_reset_attempts_for_problem_loaded_via_ajax(self):
|
||||
"""
|
||||
@@ -203,8 +215,7 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
self.assertEqual(u'Successfully reset the attempts for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_rescore_state_for_problem_loaded_via_ajax(self):
|
||||
"""
|
||||
@@ -217,7 +228,7 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.rescore()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully rescored problem for user STAFF_TESTER', msg)
|
||||
self.assertEqual(u'Successfully rescored problem for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_student_state_delete_for_problem_loaded_via_ajax(self):
|
||||
"""
|
||||
@@ -230,8 +241,7 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.delete_state()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
self.assertEqual(u'Successfully deleted student state for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
|
||||
class CourseWithContentGroupsTest(StaffViewTest):
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
End-to-end tests for the LMS that utilize the
|
||||
progress page.
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import ddt
|
||||
|
||||
from ..helpers import UniqueCourseTest, auto_auth, create_multiple_choice_problem
|
||||
from ..helpers import (
|
||||
UniqueCourseTest, auto_auth, create_multiple_choice_problem, create_multiple_choice_xml, get_modal_alert
|
||||
)
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from ...pages.common.logout import LogoutPage
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from ...pages.lms.instructor_dashboard import InstructorDashboardPage
|
||||
from ...pages.lms.instructor_dashboard import InstructorDashboardPage, StudentSpecificAdmin
|
||||
from ...pages.lms.problem import ProblemPage
|
||||
from ...pages.lms.progress import ProgressPage
|
||||
from ...pages.lms.staff_view import StaffPage, StaffDebugPage
|
||||
from ...pages.studio.component_editor import ComponentEditorView
|
||||
from ...pages.studio.utils import type_in_codemirror
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
@@ -56,13 +56,13 @@ class ProgressPageBaseTest(UniqueCourseTest):
|
||||
self.course_info['display_name']
|
||||
)
|
||||
|
||||
self.problem1 = create_multiple_choice_problem(self.PROBLEM_NAME)
|
||||
self.problem2 = create_multiple_choice_problem(self.PROBLEM_NAME_2)
|
||||
|
||||
self.course_fix.add_children(
|
||||
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),
|
||||
create_multiple_choice_problem(self.PROBLEM_NAME_2)
|
||||
)
|
||||
XBlockFixtureDesc('vertical', self.UNIT_NAME).add_children(self.problem1, self.problem2)
|
||||
)
|
||||
)
|
||||
).install()
|
||||
@@ -74,8 +74,14 @@ class ProgressPageBaseTest(UniqueCourseTest):
|
||||
"""
|
||||
Submit a correct answer to the problem.
|
||||
"""
|
||||
self._answer_problem(choice=2)
|
||||
|
||||
def _answer_problem(self, choice):
|
||||
"""
|
||||
Submit the given choice for the problem.
|
||||
"""
|
||||
self.courseware_page.go_to_sequential_position(1)
|
||||
self.problem_page.click_choice('choice_choice_2')
|
||||
self.problem_page.click_choice('choice_choice_{}'.format(choice))
|
||||
self.problem_page.click_submit()
|
||||
|
||||
def _get_section_score(self):
|
||||
@@ -92,19 +98,6 @@ class ProgressPageBaseTest(UniqueCourseTest):
|
||||
self.progress_page.visit()
|
||||
return self.progress_page.scores(self.SECTION_NAME, self.SUBSECTION_NAME)
|
||||
|
||||
def _check_progress_page_with_scored_problem(self):
|
||||
"""
|
||||
Checks the progress page before and after answering
|
||||
the course's first problem correctly.
|
||||
"""
|
||||
with self._logged_in_session():
|
||||
self.assertEqual(self._get_problem_scores(), [(0, 1), (0, 1)])
|
||||
self.assertEqual(self._get_section_score(), (0, 2))
|
||||
self.courseware_page.visit()
|
||||
self._answer_problem_correctly()
|
||||
self.assertEqual(self._get_problem_scores(), [(1, 1), (0, 1)])
|
||||
self.assertEqual(self._get_section_score(), (1, 2))
|
||||
|
||||
@contextmanager
|
||||
def _logged_in_session(self, staff=False):
|
||||
"""
|
||||
@@ -122,14 +115,6 @@ class ProgressPageBaseTest(UniqueCourseTest):
|
||||
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):
|
||||
self._check_progress_page_with_scored_problem()
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class PersistentGradesTest(ProgressPageBaseTest):
|
||||
"""
|
||||
@@ -145,104 +130,132 @@ class PersistentGradesTest(ProgressPageBaseTest):
|
||||
Adds a unit to the subsection, which
|
||||
should not affect a persisted subsection 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()
|
||||
self.course_outline.visit()
|
||||
subsection = self.course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME)
|
||||
subsection.expand_subsection()
|
||||
subsection.add_unit()
|
||||
subsection.publish()
|
||||
|
||||
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)
|
||||
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 _get_problem_in_studio(self):
|
||||
"""
|
||||
Returns the editable problem component in studio,
|
||||
along with its container unit, so any changes can
|
||||
be published.
|
||||
"""
|
||||
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]
|
||||
return unit, component
|
||||
|
||||
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()
|
||||
component = unit.xblocks[1]
|
||||
component.edit()
|
||||
component_editor = ComponentEditorView(self.browser, component.locator)
|
||||
component_editor.set_field_value_and_save('Problem Weight', 5)
|
||||
unit, component = self._get_problem_in_studio()
|
||||
component.edit()
|
||||
component_editor = ComponentEditorView(self.browser, component.locator)
|
||||
component_editor.set_field_value_and_save('Problem Weight', 5)
|
||||
unit.publish()
|
||||
|
||||
def _edit_problem_content(self):
|
||||
def _change_correct_answer_for_problem(self, new_correct_choice=1):
|
||||
"""
|
||||
Replaces the content of a problem with other html.
|
||||
Should not affect persisted grades.
|
||||
Changes the correct answer of the problem.
|
||||
"""
|
||||
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]
|
||||
modal = component.edit()
|
||||
unit, component = self._get_problem_in_studio()
|
||||
modal = component.edit()
|
||||
|
||||
# Set content in the CodeMirror editor.
|
||||
modified_content = "<p>modified content</p>"
|
||||
type_in_codemirror(self, 0, modified_content)
|
||||
modal.q(css='.action-save').click()
|
||||
modified_content = create_multiple_choice_xml(correct_choice=new_correct_choice)
|
||||
|
||||
def _delete_student_state_for_problem(self):
|
||||
type_in_codemirror(self, 0, modified_content)
|
||||
modal.q(css='.action-save').click()
|
||||
unit.publish()
|
||||
|
||||
def _student_admin_action_for_problem(self, action_button, has_cancellable_alert=False):
|
||||
"""
|
||||
As staff, clicks the "delete student state" button,
|
||||
deleting the student user's state for the problem.
|
||||
"""
|
||||
with self._logged_in_session(staff=True):
|
||||
self.instructor_dashboard_page.visit()
|
||||
student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentSpecificAdmin)
|
||||
student_admin_section.set_student_email_or_username(self.USERNAME)
|
||||
student_admin_section.set_problem_location(self.problem1.locator)
|
||||
getattr(student_admin_section, action_button).click()
|
||||
if has_cancellable_alert:
|
||||
alert = get_modal_alert(student_admin_section.browser)
|
||||
alert.accept()
|
||||
alert = get_modal_alert(student_admin_section.browser)
|
||||
alert.dismiss()
|
||||
return student_admin_section
|
||||
|
||||
def test_progress_page_shows_scored_problems(self):
|
||||
"""
|
||||
Checks the progress page before and after answering
|
||||
the course's first problem correctly.
|
||||
"""
|
||||
with self._logged_in_session():
|
||||
self.assertEqual(self._get_problem_scores(), [(0, 1), (0, 1)])
|
||||
self.assertEqual(self._get_section_score(), (0, 2))
|
||||
self.courseware_page.visit()
|
||||
staff_page = StaffPage(self.browser, self.course_id)
|
||||
self.assertEqual(staff_page.staff_view_mode, "Staff")
|
||||
staff_page.q(css='a.instructor-info-action').nth(1).click()
|
||||
staff_debug_page = StaffDebugPage(self.browser)
|
||||
staff_debug_page.wait_for_page()
|
||||
staff_debug_page.delete_state(self.USERNAME)
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state for user {0}'.format(self.USERNAME), msg)
|
||||
self._answer_problem_correctly()
|
||||
self.assertEqual(self._get_problem_scores(), [(1, 1), (0, 1)])
|
||||
self.assertEqual(self._get_section_score(), (1, 2))
|
||||
|
||||
@ddt.data(
|
||||
_edit_problem_content,
|
||||
_change_correct_answer_for_problem,
|
||||
_change_subsection_structure,
|
||||
_change_weight_for_problem
|
||||
)
|
||||
def test_content_changes_do_not_change_score(self, edit):
|
||||
with self._logged_in_session():
|
||||
self._check_progress_page_with_scored_problem()
|
||||
self.courseware_page.visit()
|
||||
self._answer_problem_correctly()
|
||||
|
||||
edit(self)
|
||||
with self._logged_in_session(staff=True):
|
||||
edit(self)
|
||||
|
||||
with self._logged_in_session():
|
||||
self.assertEqual(self._get_problem_scores(), [(1, 1), (0, 1)])
|
||||
self.assertEqual(self._get_section_score(), (1, 2))
|
||||
|
||||
def test_visibility_change_does_affect_score(self):
|
||||
def test_visibility_change_affects_score(self):
|
||||
with self._logged_in_session():
|
||||
self._check_progress_page_with_scored_problem()
|
||||
self.courseware_page.visit()
|
||||
self._answer_problem_correctly()
|
||||
|
||||
self._set_staff_lock_on_subsection(True)
|
||||
with self._logged_in_session(staff=True):
|
||||
self._set_staff_lock_on_subsection(True)
|
||||
|
||||
with self._logged_in_session():
|
||||
self.assertEqual(self._get_problem_scores(), None)
|
||||
self.assertEqual(self._get_section_score(), None)
|
||||
|
||||
self._set_staff_lock_on_subsection(False)
|
||||
with self._logged_in_session(staff=True):
|
||||
self._set_staff_lock_on_subsection(False)
|
||||
|
||||
with self._logged_in_session():
|
||||
self.assertEqual(self._get_problem_scores(), [(1, 1), (0, 1)])
|
||||
self.assertEqual(self._get_section_score(), (1, 2))
|
||||
|
||||
def test_progress_page_updates_when_student_state_deleted(self):
|
||||
self._check_progress_page_with_scored_problem()
|
||||
self._delete_student_state_for_problem()
|
||||
def test_delete_student_state_affects_score(self):
|
||||
with self._logged_in_session():
|
||||
self.courseware_page.visit()
|
||||
self._answer_problem_correctly()
|
||||
|
||||
with self._logged_in_session(staff=True):
|
||||
self._student_admin_action_for_problem('delete_state_button', has_cancellable_alert=True)
|
||||
|
||||
with self._logged_in_session():
|
||||
self.assertEqual(self._get_problem_scores(), [(0, 1), (0, 1)])
|
||||
self.assertEqual(self._get_section_score(), (0, 2))
|
||||
|
||||
Reference in New Issue
Block a user