Support for rescoring a problem only if the new score is higher

TNL-5046
This commit is contained in:
Nimisha Asthagiri
2016-10-04 08:14:10 -04:00
parent 0e3e1b479a
commit aa000c1a3d
45 changed files with 1263 additions and 991 deletions

View File

@@ -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']

View File

@@ -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')

View File

@@ -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):

View File

@@ -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):
"""

View File

@@ -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).

View File

@@ -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,

View File

@@ -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)

View File

@@ -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):

View File

@@ -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))