Implemented Validation for Course Advanced Setting
This commit adds validation for course advanced settings. Currently when course
administrators make invalid changes in the Settings/Advanced Settings tab,
they're not notified through a new modal window of the list of invalid settings
changes.
* Extending CourseMetadata
- Previously, we only had update_from_json method in CourseMetadata.py,
and it was only validating one field every POST request.
- Now we have validate_and_update_from_json method that encapsulates the
functionality of update_from_json into a validation call
- To avoid discrepancy of validation standards between modules, validation
uses the from_json method implemented to each field in xblock.
* Different Response in advanced settings ajax requests
- After receiving a POST ajax request, course.py calls
validate_and_update_from_json, and sends a json object of either:
1) valid course metadata model
2) error objects
* Error Messages shown in validation-error-modal
- error objects passed through ajax are shown in a separate modal.
This commit is contained in:
@@ -7,7 +7,11 @@ from .utils import press_the_notification_button, type_in_codemirror, get_codemi
|
||||
|
||||
|
||||
KEY_CSS = '.key h3.title'
|
||||
|
||||
UNDO_BUTTON_SELECTOR = ".action-item .action-undo"
|
||||
MANUAL_BUTTON_SELECTOR = ".action-item .action-cancel"
|
||||
MODAL_SELECTOR = ".validation-error-modal-content"
|
||||
ERROR_ITEM_NAME_SELECTOR = ".error-item-title strong"
|
||||
ERROR_ITEM_CONTENT_SELECTOR = ".error-item-message"
|
||||
|
||||
class AdvancedSettingsPage(CoursePage):
|
||||
"""
|
||||
@@ -19,6 +23,57 @@ class AdvancedSettingsPage(CoursePage):
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='body.advanced').present
|
||||
|
||||
def wait_for_modal_load(self):
|
||||
"""
|
||||
Wait for validation response from the server, and make sure that
|
||||
the validation error modal pops up.
|
||||
|
||||
This method should only be called when it is guaranteed that there're
|
||||
validation errors in the settings changes.
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
self.wait_for_element_presence(MODAL_SELECTOR, 'Validation Modal is present')
|
||||
|
||||
def refresh_and_wait_for_load(self):
|
||||
"""
|
||||
Refresh the page and wait for all resources to load.
|
||||
"""
|
||||
self.browser.refresh()
|
||||
self.wait_for_page()
|
||||
|
||||
def undo_changes_via_modal(self):
|
||||
"""
|
||||
Trigger clicking event of the undo changes button in the modal.
|
||||
Wait for the undoing process to load via ajax call.
|
||||
"""
|
||||
self.q(css=UNDO_BUTTON_SELECTOR).click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def trigger_manual_changes(self):
|
||||
"""
|
||||
Trigger click event of the manual changes button in the modal.
|
||||
No need to wait for any ajax.
|
||||
"""
|
||||
self.q(css=MANUAL_BUTTON_SELECTOR).click()
|
||||
|
||||
def is_validation_modal_present(self):
|
||||
"""
|
||||
Checks if the validation modal is present.
|
||||
"""
|
||||
return self.q(css=MODAL_SELECTOR).present
|
||||
|
||||
def get_error_item_names(self):
|
||||
"""
|
||||
Returns a list of display names of all invalid settings.
|
||||
"""
|
||||
return self.q(css=ERROR_ITEM_NAME_SELECTOR).text
|
||||
|
||||
def get_error_item_messages(self):
|
||||
"""
|
||||
Returns a list of error messages of all invalid settings.
|
||||
"""
|
||||
return self.q(css=ERROR_ITEM_CONTENT_SELECTOR).text
|
||||
|
||||
def _get_index_of(self, expected_key):
|
||||
for i, element in enumerate(self.q(css=KEY_CSS)):
|
||||
# Sometimes get stale reference if I hold on to the array of elements
|
||||
@@ -42,3 +97,26 @@ class AdvancedSettingsPage(CoursePage):
|
||||
def get(self, key):
|
||||
index = self._get_index_of(key)
|
||||
return get_codemirror_value(self, index)
|
||||
|
||||
def set_values(self, key_value_map):
|
||||
"""
|
||||
Make multiple settings changes and save them.
|
||||
"""
|
||||
for key, value in key_value_map.iteritems():
|
||||
index = self._get_index_of(key)
|
||||
type_in_codemirror(self, index, value)
|
||||
|
||||
self.save()
|
||||
|
||||
def get_values(self, key_list):
|
||||
"""
|
||||
Get a key-value dictionary of all keys in the given list.
|
||||
"""
|
||||
result_map = {}
|
||||
|
||||
for key in key_list:
|
||||
index = self._get_index_of(key)
|
||||
val = get_codemirror_value(self, index)
|
||||
result_map[key] = val
|
||||
|
||||
return result_map
|
||||
|
||||
171
common/test/acceptance/tests/test_studio_settings.py
Normal file
171
common/test/acceptance/tests/test_studio_settings.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Acceptance tests for Studio's Setting pages
|
||||
"""
|
||||
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from acceptance.tests.base_studio_test import StudioCourseTest
|
||||
|
||||
from ..pages.studio.settings_advanced import AdvancedSettingsPage
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class AdvancedSettingsValidationTest(StudioCourseTest):
|
||||
"""
|
||||
Tests for validation feature in Studio's advanced settings tab
|
||||
"""
|
||||
def setUp(self):
|
||||
super(AdvancedSettingsValidationTest, self).setUp()
|
||||
self.advanced_settings = AdvancedSettingsPage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
self.course_info['run']
|
||||
)
|
||||
|
||||
self.type_fields = ['Course Display Name', 'Advanced Module List', 'Discussion Topic Mapping',
|
||||
'Maximum Attempts', 'Course Announcement Date']
|
||||
|
||||
# Before every test, make sure to visit the page first
|
||||
self.advanced_settings.visit()
|
||||
self.assertTrue(self.advanced_settings.is_browser_on_page())
|
||||
|
||||
def test_modal_shows_one_validation_error(self):
|
||||
"""
|
||||
Test that advanced settings don't save if there's a single wrong input,
|
||||
and that it shows the correct error message in the modal.
|
||||
"""
|
||||
|
||||
# Feed an integer value for String field.
|
||||
# .set method saves automatically after setting a value
|
||||
course_display_name = self.advanced_settings.get('Course Display Name')
|
||||
self.advanced_settings.set('Course Display Name', 1)
|
||||
self.advanced_settings.wait_for_modal_load()
|
||||
|
||||
# Test Modal
|
||||
self.check_modal_shows_correct_contents(['Course Display Name'])
|
||||
self.advanced_settings.refresh_and_wait_for_load()
|
||||
|
||||
self.assertEquals(
|
||||
self.advanced_settings.get('Course Display Name'),
|
||||
course_display_name,
|
||||
'Wrong input for Course Display Name must not change its value'
|
||||
)
|
||||
|
||||
def test_modal_shows_multiple_validation_errors(self):
|
||||
"""
|
||||
Test that advanced settings don't save with multiple wrong inputs
|
||||
"""
|
||||
|
||||
# Save original values and feed wrong inputs
|
||||
original_values_map = self.get_settings_fields_of_each_type()
|
||||
self.set_wrong_inputs_to_fields()
|
||||
self.advanced_settings.wait_for_modal_load()
|
||||
|
||||
# Test Modal
|
||||
self.check_modal_shows_correct_contents(self.type_fields)
|
||||
self.advanced_settings.refresh_and_wait_for_load()
|
||||
|
||||
for key, val in original_values_map.iteritems():
|
||||
self.assertEquals(
|
||||
self.advanced_settings.get(key),
|
||||
val,
|
||||
'Wrong input for Advanced Settings Fields must not change its value'
|
||||
)
|
||||
|
||||
def test_undo_changes(self):
|
||||
"""
|
||||
Test that undo changes button in the modal resets all settings changes
|
||||
"""
|
||||
|
||||
# Save original values and feed wrong inputs
|
||||
original_values_map = self.get_settings_fields_of_each_type()
|
||||
self.set_wrong_inputs_to_fields()
|
||||
|
||||
# Let modal popup
|
||||
self.advanced_settings.wait_for_modal_load()
|
||||
|
||||
# Press Undo Changes button
|
||||
self.advanced_settings.undo_changes_via_modal()
|
||||
|
||||
# Check that changes are undone
|
||||
for key, val in original_values_map.iteritems():
|
||||
self.assertEquals(
|
||||
self.advanced_settings.get(key),
|
||||
val,
|
||||
'Undoing Should revert back to original value'
|
||||
)
|
||||
|
||||
def test_manual_change(self):
|
||||
"""
|
||||
Test that manual changes button in the modal keeps settings unchanged
|
||||
"""
|
||||
inputs = {"Course Display Name": 1,
|
||||
"Advanced Module List": 1,
|
||||
"Discussion Topic Mapping": 1,
|
||||
"Maximum Attempts": '"string"',
|
||||
"Course Announcement Date": '"string"',
|
||||
}
|
||||
|
||||
self.set_wrong_inputs_to_fields()
|
||||
self.advanced_settings.wait_for_modal_load()
|
||||
self.advanced_settings.trigger_manual_changes()
|
||||
|
||||
# Check that the validation modal went away.
|
||||
self.assertFalse(self.advanced_settings.is_validation_modal_present())
|
||||
|
||||
# Iterate through the wrong values and make sure they're still displayed
|
||||
for key, val in inputs.iteritems():
|
||||
print self.advanced_settings.get(key)
|
||||
print val
|
||||
self.assertEquals(
|
||||
str(self.advanced_settings.get(key)),
|
||||
str(val),
|
||||
'manual change should keep: ' + str(val) + ', but is: ' + str(self.advanced_settings.get(key))
|
||||
)
|
||||
|
||||
def check_modal_shows_correct_contents(self, wrong_settings_list):
|
||||
"""
|
||||
Helper function that checks if the validation modal contains correct
|
||||
error messages.
|
||||
"""
|
||||
# Check presence of modal
|
||||
self.assertTrue(self.advanced_settings.is_validation_modal_present())
|
||||
|
||||
# List of wrong settings item & what is presented in the modal should be the same
|
||||
error_item_names = self.advanced_settings.get_error_item_names()
|
||||
self.assertEqual(set(wrong_settings_list), set(error_item_names))
|
||||
|
||||
error_item_messages = self.advanced_settings.get_error_item_messages()
|
||||
self.assertEqual(len(error_item_names), len(error_item_messages))
|
||||
|
||||
def get_settings_fields_of_each_type(self):
|
||||
"""
|
||||
Get one of each field type:
|
||||
- String: Course Display Name
|
||||
- List: Advanced Module List
|
||||
- Dict: Discussion Topic Mapping
|
||||
- Integer: Maximum Attempts
|
||||
- Date: Course Announcement Date
|
||||
"""
|
||||
return {
|
||||
"Course Display Name": self.advanced_settings.get('Course Display Name'),
|
||||
"Advanced Module List": self.advanced_settings.get('Advanced Module List'),
|
||||
"Discussion Topic Mapping": self.advanced_settings.get('Discussion Topic Mapping'),
|
||||
"Maximum Attempts": self.advanced_settings.get('Maximum Attempts'),
|
||||
"Course Announcement Date": self.advanced_settings.get('Course Announcement Date'),
|
||||
}
|
||||
|
||||
def set_wrong_inputs_to_fields(self):
|
||||
"""
|
||||
Set wrong values for the chosen fields
|
||||
"""
|
||||
self.advanced_settings.set_values(
|
||||
{
|
||||
"Course Display Name": 1,
|
||||
"Advanced Module List": 1,
|
||||
"Discussion Topic Mapping": 1,
|
||||
"Maximum Attempts": '"string"',
|
||||
"Course Announcement Date": '"string"',
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user