Commit 890e25f4 by Andy Armstrong

Merge pull request #4454 from Stanford-Online/sjang92/advanced_settings_feedback

Sjang92/advanced settings feedback
parents 4ecd0458 11d26091
...@@ -82,8 +82,8 @@ def it_is_formatted(step): ...@@ -82,8 +82,8 @@ def it_is_formatted(step):
@step('I get an error on save$') @step('I get an error on save$')
def error_on_save(step): def error_on_save(step):
assert_regexp_matches( assert_regexp_matches(
world.css_text('#notification-error-description'), world.css_text('.error-item-message'),
"Incorrect format for field '{}'.".format(DISPLAY_NAME_KEY) "Value stored in a .* must be .*, found .*"
) )
......
...@@ -458,6 +458,69 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -458,6 +458,69 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertIn('showanswer', test_model, 'showanswer field ') self.assertIn('showanswer', test_model, 'showanswer field ')
self.assertIn('xqa_key', test_model, 'xqa_key field ') self.assertIn('xqa_key', test_model, 'xqa_key field ')
def test_validate_and_update_from_json_correct_inputs(self):
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
{
"advertised_start": {"value": "start A"},
"days_early_for_beta": {"value": 2},
"advanced_modules": {"value": ['combinedopenended']},
},
user=self.user
)
self.assertTrue(is_valid)
self.assertTrue(len(errors) == 0)
self.update_check(test_model)
# fresh fetch to ensure persistence
fresh = modulestore().get_course(self.course.id)
test_model = CourseMetadata.fetch(fresh)
self.update_check(test_model)
# Tab gets tested in test_advanced_settings_munge_tabs
self.assertIn('advanced_modules', test_model, 'Missing advanced_modules')
self.assertEqual(test_model['advanced_modules']['value'], ['combinedopenended'], 'advanced_module is not updated')
def test_validate_and_update_from_json_wrong_inputs(self):
# input incorrectly formatted data
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
{
"advertised_start": {"value": 1, "display_name": "Course Advertised Start Date", },
"days_early_for_beta": {"value": "supposed to be an integer",
"display_name": "Days Early for Beta Users", },
"advanced_modules": {"value": 1, "display_name": "Advanced Module List", },
},
user=self.user
)
# Check valid results from validate_and_update_from_json
self.assertFalse(is_valid)
self.assertEqual(len(errors), 3)
self.assertFalse(test_model)
error_keys = set([error_obj['model']['display_name'] for error_obj in errors])
test_keys = set(['Advanced Module List', 'Course Advertised Start Date', 'Days Early for Beta Users'])
self.assertEqual(error_keys, test_keys)
# try fresh fetch to ensure no update happened
fresh = modulestore().get_course(self.course.id)
test_model = CourseMetadata.fetch(fresh)
self.assertNotEqual(test_model['advertised_start']['value'], 1, 'advertised_start should not be updated to a wrong value')
self.assertNotEqual(test_model['days_early_for_beta']['value'], "supposed to be an integer",
'days_early_for beta should not be updated to a wrong value')
def test_correct_http_status(self):
json_data = json.dumps({
"advertised_start": {"value": 1, "display_name": "Course Advertised Start Date", },
"days_early_for_beta": {"value": "supposed to be an integer",
"display_name": "Days Early for Beta Users", },
"advanced_modules": {"value": 1, "display_name": "Advanced Module List", },
})
response = self.client.ajax_post(self.course_setting_url, json_data)
self.assertEqual(400, response.status_code)
def test_update_from_json(self): def test_update_from_json(self):
test_model = CourseMetadata.update_from_json( test_model = CourseMetadata.update_from_json(
self.course, self.course,
...@@ -487,6 +550,9 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -487,6 +550,9 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value") self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value")
def update_check(self, test_model): def update_check(self, test_model):
"""
checks that updates were made
"""
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value")
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field') self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
......
...@@ -13,7 +13,7 @@ from django.views.decorators.http import require_http_methods ...@@ -13,7 +13,7 @@ from django.views.decorators.http import require_http_methods
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponse from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponse
from util.json_request import JsonResponse from util.json_request import JsonResponse, JsonResponseBadRequest
from util.date_utils import get_default_time_display from util.date_utils import get_default_time_display
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
...@@ -834,18 +834,26 @@ def _config_course_advanced_components(request, course_module): ...@@ -834,18 +834,26 @@ def _config_course_advanced_components(request, course_module):
component_types = tab_component_map.get(tab_type) component_types = tab_component_map.get(tab_type)
found_ac_type = False found_ac_type = False
for ac_type in component_types: for ac_type in component_types:
if ac_type in request.json[ADVANCED_COMPONENT_POLICY_KEY]["value"] and ac_type in ADVANCED_COMPONENT_TYPES:
# Add tab to the course if needed # Check if the user has incorrectly failed to put the value in an iterable.
changed, new_tabs = add_extra_panel_tab(tab_type, course_module) new_advanced_component_list = request.json[ADVANCED_COMPONENT_POLICY_KEY]['value']
# If a tab has been added to the course, then send the if hasattr(new_advanced_component_list, '__iter__'):
# metadata along to CourseMetadata.update_from_json if ac_type in new_advanced_component_list and ac_type in ADVANCED_COMPONENT_TYPES:
if changed:
course_module.tabs = new_tabs # Add tab to the course if needed
request.json.update({'tabs': {'value': new_tabs}}) changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
# Indicate that tabs should not be filtered out of # If a tab has been added to the course, then send the
# the metadata # metadata along to CourseMetadata.update_from_json
filter_tabs = False # Set this flag to avoid the tab removal code below. if changed:
found_ac_type = True # break course_module.tabs = new_tabs
request.json.update({'tabs': {'value': new_tabs}})
# Indicate that tabs should not be filtered out of
# the metadata
filter_tabs = False # Set this flag to avoid the tab removal code below.
found_ac_type = True # break
else:
# If not iterable, return immediately and let validation handle.
return
# If we did not find a module type in the advanced settings, # If we did not find a module type in the advanced settings,
# we may need to remove the tab from the course. # we may need to remove the tab from the course.
...@@ -891,12 +899,21 @@ def advanced_settings_handler(request, course_key_string): ...@@ -891,12 +899,21 @@ def advanced_settings_handler(request, course_key_string):
try: try:
# Whether or not to filter the tabs key out of the settings metadata # Whether or not to filter the tabs key out of the settings metadata
filter_tabs = _config_course_advanced_components(request, course_module) filter_tabs = _config_course_advanced_components(request, course_module)
return JsonResponse(CourseMetadata.update_from_json(
# validate data formats and update
is_valid, errors, updated_data = CourseMetadata.validate_and_update_from_json(
course_module, course_module,
request.json, request.json,
filter_tabs=filter_tabs, filter_tabs=filter_tabs,
user=request.user, user=request.user,
)) )
if is_valid:
return JsonResponse(updated_data)
else:
return JsonResponseBadRequest(errors)
# Handle all errors that validation doesn't catch
except (TypeError, ValueError) as err: except (TypeError, ValueError) as err:
return HttpResponseBadRequest( return HttpResponseBadRequest(
django.utils.html.escape(err.message), django.utils.html.escape(err.message),
......
...@@ -82,10 +82,55 @@ class CourseMetadata(object): ...@@ -82,10 +82,55 @@ class CourseMetadata(object):
raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}".format( raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}".format(
name=model['display_name'], detailed_message=err.message))) name=model['display_name'], detailed_message=err.message)))
return cls.update_from_dict(key_values, descriptor, user)
@classmethod
def validate_and_update_from_json(cls, descriptor, jsondict, user, filter_tabs=True):
"""
Validate the values in the json dict (validated by xblock fields from_json method)
If all fields validate, go ahead and update those values in the database.
If not, return the error objects list.
Returns:
did_validate: whether values pass validation or not
errors: list of error objects
result: the updated course metadata or None if error
"""
filtered_list = list(cls.FILTERED_LIST)
if not filter_tabs:
filtered_list.remove("tabs")
filtered_dict = dict((k, v) for k, v in jsondict.iteritems() if k not in filtered_list)
did_validate = True
errors = []
key_values = {}
updated_data = None
for key, model in filtered_dict.iteritems():
try:
val = model['value']
if hasattr(descriptor, key) and getattr(descriptor, key) != val:
key_values[key] = descriptor.fields[key].from_json(val)
except (TypeError, ValueError) as err:
did_validate = False
errors.append({'message': err.message, 'model': model})
# If did validate, go ahead and update the metadata
if did_validate:
updated_data = cls.update_from_dict(key_values, descriptor, user)
return did_validate, errors, updated_data
@classmethod
def update_from_dict(cls, key_values, descriptor, user):
"""
Update metadata descriptor in modulestore from key_values.
"""
for key, value in key_values.iteritems(): for key, value in key_values.iteritems():
setattr(descriptor, key, value) setattr(descriptor, key, value)
if len(key_values) > 0: if len(key_values):
modulestore().update_item(descriptor, user.id) modulestore().update_item(descriptor, user.id)
return cls.fetch(descriptor) return cls.fetch(descriptor)
...@@ -238,6 +238,7 @@ define([ ...@@ -238,6 +238,7 @@ define([
"js/spec/views/modals/base_modal_spec", "js/spec/views/modals/base_modal_spec",
"js/spec/views/modals/edit_xblock_spec", "js/spec/views/modals/edit_xblock_spec",
"js/spec/views/modals/validation_error_modal_spec",
"js/spec/xblock/cms.runtime.v1_spec", "js/spec/xblock/cms.runtime.v1_spec",
......
define(['jquery', 'underscore', 'js/spec_helpers/validation_helpers', 'js/views/modals/validation_error_modal'],
function ($, _, validation_helpers, ValidationErrorModal) {
describe('ValidationErrorModal', function() {
var modal, showModal;
showModal = function (jsonContent, callback) {
modal = new ValidationErrorModal();
modal.setResetCallback(callback);
modal.setContent(jsonContent);
modal.show();
};
/* Before each, install templates required for the base modal
and validation error modal. */
beforeEach(function () {
validation_helpers.installValidationTemplates();
});
afterEach(function() {
validation_helpers.hideModalIfShowing(modal);
});
it('is visible after show is called', function () {
showModal([]);
expect(validation_helpers.isShowingModal(modal)).toBeTruthy();
});
it('displays none if no error given', function () {
var errorObjects = [];
showModal(errorObjects);
expect(validation_helpers.isShowingModal(modal)).toBeTruthy();
validation_helpers.checkErrorContents(modal, errorObjects);
});
it('correctly displays json error message objects', function () {
var errorObjects = [
{
model: {display_name: 'test_attribute1'},
message: 'Encountered an error while saving test_attribute1'
},
{
model: {display_name: 'test_attribute2'},
message: 'Encountered an error while saving test_attribute2'
}
];
showModal(errorObjects);
expect(validation_helpers.isShowingModal(modal)).toBeTruthy();
validation_helpers.checkErrorContents(modal, errorObjects);
});
it('run callback when undo changes button is clicked', function () {
var errorObjects = [
{
model: {display_name: 'test_attribute1'},
message: 'Encountered an error while saving test_attribute1'
},
{
model: {display_name: 'test_attribute2'},
message: 'Encountered an error while saving test_attribute2'
}
];
var callback = function() {
return true;
};
// Show Modal and click undo changes
showModal(errorObjects, callback);
expect(validation_helpers.isShowingModal(modal)).toBeTruthy();
validation_helpers.undoChanges(modal);
// Wait for the callback to be fired
waitsFor(function () {
return callback();
}, 'the callback to be called', 5000);
// After checking callback fire, check modal hide
runs(function () {
expect(validation_helpers.isShowingModal(modal)).toBe(false);
});
});
});
});
/**
* Provides helper methods for invoking Validation modal in Jasmine tests.
*/
define(['jquery', 'js/spec_helpers/modal_helpers', 'js/spec_helpers/view_helpers'],
function($, modal_helpers, view_helpers) {
var installValidationTemplates, checkErrorContents, undoChanges;
installValidationTemplates = function () {
modal_helpers.installModalTemplates();
view_helpers.installTemplate('validation-error-modal');
};
checkErrorContents = function(validationModal, errorObjects) {
var errorItems = validationModal.$('.error-item-message');
var i, item;
var num_items = errorItems.length;
expect(num_items).toBe(errorObjects.length);
for (i = 0; i < num_items; i++) {
item = errorItems[i];
expect(item.value).toBe(errorObjects[i].message);
}
};
undoChanges = function(validationModal) {
modal_helpers.pressModalButton('.action-undo', validationModal);
};
return $.extend(modal_helpers, {
'installValidationTemplates': installValidationTemplates,
'checkErrorContents': checkErrorContents,
'undoChanges': undoChanges,
});
});
\ No newline at end of file
define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal'],
function($, _, gettext, BaseModal) {
var ValidationErrorModal = BaseModal.extend({
events: {
'click .action-cancel': 'cancel',
'click .action-undo': 'resetAction'
},
initialize: function() {
BaseModal.prototype.initialize.call(this);
this.template = this.loadTemplate('validation-error-modal');
},
options: $.extend({}, BaseModal.prototype.options, {
modalName: 'Validation Error Modal',
title: gettext('Validation Error While Saving'),
modalSize: 'md'
}),
addActionButtons: function() {
this.addActionButton('undo', gettext('Undo Changes'), true);
this.addActionButton('cancel', gettext('Change Manually'));
},
render: function() {
BaseModal.prototype.render.call(this);
},
/* Set the JSON object of error_models that will be displayed
* it must be an object, not json string. */
setContent: function(json_object) {
this.response = json_object;
},
/* Create the content HTML for this modal by passing necessary
* parameters to template (validation-error-modal) */
getContentHtml: function() {
return this.template({
response: this.response,
num_errors: this.response.length,
});
},
/* Receive calback function from the view, so that it can be
* invoked when the user clicks the reset button */
setResetCallback: function(reset_callback) {
this.reset_callback = reset_callback;
},
/* Upon receiving a user's clicking event on the reset button,
* resets all setting changes, and hide the modal */
resetAction: function() {
// reset page content
this.reset_callback();
// hide the modal
BaseModal.prototype.hide.call(this);
},
});
return ValidationErrorModal;
}
);
define(["js/views/validation", "jquery", "underscore", "gettext", "codemirror"], define(["js/views/validation", "jquery", "underscore", "gettext", "codemirror", "js/views/modals/validation_error_modal"],
function(ValidatingView, $, _, gettext, CodeMirror) { function(ValidatingView, $, _, gettext, CodeMirror, ValidationErrorModal) {
var AdvancedView = ValidatingView.extend({ var AdvancedView = ValidatingView.extend({
error_saving : "error_saving", error_saving : "error_saving",
...@@ -51,8 +51,8 @@ var AdvancedView = ValidatingView.extend({ ...@@ -51,8 +51,8 @@ var AdvancedView = ValidatingView.extend({
var self = this; var self = this;
var oldValue = $(textarea).val(); var oldValue = $(textarea).val();
var cm = CodeMirror.fromTextArea(textarea, { var cm = CodeMirror.fromTextArea(textarea, {
mode: "application/json", mode: "application/json",
lineNumbers: false, lineNumbers: false,
lineWrapping: false}); lineWrapping: false});
cm.on('change', function(instance, changeobj) { cm.on('change', function(instance, changeobj) {
instance.save(); instance.save();
...@@ -115,7 +115,24 @@ var AdvancedView = ValidatingView.extend({ ...@@ -115,7 +115,24 @@ var AdvancedView = ValidatingView.extend({
'course': course_location_analytics 'course': course_location_analytics
}); });
}, },
silent: true silent: true,
error: function(model, response, options) {
var json_response, reset_callback, err_modal;
/* Check that the server came back with a bad request error*/
if (response.status === 400) {
json_response = $.parseJSON(response.responseText);
reset_callback = function() {
self.revertView();
};
/* initialize and show validation error modal */
err_modal = new ValidationErrorModal();
err_modal.setContent(json_response);
err_modal.setResetCallback(reset_callback);
err_modal.show();
}
}
}); });
}, },
revertView: function() { revertView: function() {
......
...@@ -899,4 +899,40 @@ ...@@ -899,4 +899,40 @@
.content-supplementary { .content-supplementary {
width: flex-grid(3, 12); width: flex-grid(3, 12);
} }
.wrapper-modal-window {
.validation-error-modal-content {
.error-header {
p {
strong {
color: $error-red;
}
}
}
hr {
margin: 25px 0;
}
.error-list {
.error-item {
.error-item-title {
color: $error-red;
}
.error-item-message {
width:100%;
border: none;
resize: none;
&:focus {
outline: 0;
}
}
}
}
}
}
} }
<div class = "validation-error-modal-content">
<div class "error-header">
<p>
<%= _.template(
gettext("There were {strong_start}{num_errors} validation error(s){strong_end} while trying to save the course setting(s) in the database."),
{
strong_start:'<strong>',
num_errors: num_errors,
strong_end: '</strong>'
},
{interpolate: /\{(.+?)\}/g})%>
<%= gettext("Please check the following validation feedbacks and reflect them in your course settings:")%></p>
</div>
<hr>
<ul class = "error-list">
<% _.each(response, function(value, index, list) { %>
<li class = "error-item">
<span class='error-item-title'>
<i class="icon-warning-sign"></i>
<strong><%= value.model.display_name %></strong>:
</span>
<textarea class = "error-item-message" disabled='disabled'><%=value.message%></textarea>
</li>
<% }); %>
</ul>
</div>
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<%block name="bodyclass">is-signedin course advanced view-settings</%block> <%block name="bodyclass">is-signedin course advanced view-settings</%block>
<%block name="jsextra"> <%block name="jsextra">
% for template_name in ["advanced_entry"]: % for template_name in ["advanced_entry", "basic-modal", "modal-button", "validation-error-modal"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" /> <%static:include path="js/${template_name}.underscore" />
</script> </script>
......
...@@ -3,7 +3,7 @@ import json ...@@ -3,7 +3,7 @@ import json
from django.core.serializers import serialize from django.core.serializers import serialize
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.http import HttpResponse from django.http import HttpResponse, HttpResponseBadRequest
def expect_json(view_function): def expect_json(view_function):
...@@ -43,3 +43,23 @@ class JsonResponse(HttpResponse): ...@@ -43,3 +43,23 @@ class JsonResponse(HttpResponse):
if status: if status:
kwargs["status"] = status kwargs["status"] = status
super(JsonResponse, self).__init__(content, *args, **kwargs) super(JsonResponse, self).__init__(content, *args, **kwargs)
class JsonResponseBadRequest(HttpResponseBadRequest):
"""
Subclass of HttpResponseBadRequest that defaults to outputting JSON.
Use this to send BadRequestResponse & some Json object along with it.
Defaults:
dictionary: empty dictionary
status: 400
encoder: DjangoJSONEncoder
"""
def __init__(self, obj=None, status=400, encoder=DjangoJSONEncoder, *args, **kwargs):
if obj in (None, ""):
content = ""
else:
content = json.dumps(obj, cls=encoder, indent=2, ensure_ascii=False)
kwargs.setdefault("content_type", "application/json")
kwargs["status"] = status
super(JsonResponseBadRequest, self).__init__(content, *args, **kwargs)
from django.http import HttpResponse """
from util.json_request import JsonResponse Test for JsonResponse and JsonResponseBadRequest util classes.
"""
from django.http import HttpResponse, HttpResponseBadRequest
from util.json_request import JsonResponse, JsonResponseBadRequest
import json import json
import unittest import unittest
import mock import mock
class JsonResponseTestCase(unittest.TestCase): class JsonResponseTestCase(unittest.TestCase):
"""
A set of tests to make sure that JsonResponse Class works correctly.
"""
def test_empty(self): def test_empty(self):
resp = JsonResponse() resp = JsonResponse()
self.assertIsInstance(resp, HttpResponse) self.assertIsInstance(resp, HttpResponse)
...@@ -60,3 +67,59 @@ class JsonResponseTestCase(unittest.TestCase): ...@@ -60,3 +67,59 @@ class JsonResponseTestCase(unittest.TestCase):
self.assertEqual(obj, compare) self.assertEqual(obj, compare)
kwargs = dumps.call_args[1] kwargs = dumps.call_args[1]
self.assertIs(kwargs["cls"], encoder) self.assertIs(kwargs["cls"], encoder)
class JsonResponseBadRequestTestCase(unittest.TestCase):
"""
A set of tests to make sure that the JsonResponseBadRequest wrapper class
works as intended.
"""
def test_empty(self):
resp = JsonResponseBadRequest()
self.assertIsInstance(resp, HttpResponseBadRequest)
self.assertEqual(resp.content, "")
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp["content-type"], "application/json")
def test_empty_string(self):
resp = JsonResponseBadRequest("")
self.assertIsInstance(resp, HttpResponse)
self.assertEqual(resp.content, "")
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp["content-type"], "application/json")
def test_dict(self):
obj = {"foo": "bar"}
resp = JsonResponseBadRequest(obj)
compare = json.loads(resp.content)
self.assertEqual(obj, compare)
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp["content-type"], "application/json")
def test_set_status_kwarg(self):
obj = {"error": "resource not found"}
resp = JsonResponseBadRequest(obj, status=404)
compare = json.loads(resp.content)
self.assertEqual(obj, compare)
self.assertEqual(resp.status_code, 404)
self.assertEqual(resp["content-type"], "application/json")
def test_set_status_arg(self):
obj = {"error": "resource not found"}
resp = JsonResponseBadRequest(obj, 404)
compare = json.loads(resp.content)
self.assertEqual(obj, compare)
self.assertEqual(resp.status_code, 404)
self.assertEqual(resp["content-type"], "application/json")
def test_encoder(self):
obj = [1, 2, 3]
encoder = object()
with mock.patch.object(json, "dumps", return_value="[1,2,3]") as dumps:
resp = JsonResponseBadRequest(obj, encoder=encoder)
self.assertEqual(resp.status_code, 400)
compare = json.loads(resp.content)
self.assertEqual(obj, compare)
kwargs = dumps.call_args[1]
self.assertIs(kwargs["cls"], encoder)
...@@ -7,7 +7,11 @@ from .utils import press_the_notification_button, type_in_codemirror, get_codemi ...@@ -7,7 +7,11 @@ from .utils import press_the_notification_button, type_in_codemirror, get_codemi
KEY_CSS = '.key h3.title' 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): class AdvancedSettingsPage(CoursePage):
""" """
...@@ -19,6 +23,57 @@ class AdvancedSettingsPage(CoursePage): ...@@ -19,6 +23,57 @@ class AdvancedSettingsPage(CoursePage):
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css='body.advanced').present 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): def _get_index_of(self, expected_key):
for i, element in enumerate(self.q(css=KEY_CSS)): for i, element in enumerate(self.q(css=KEY_CSS)):
# Sometimes get stale reference if I hold on to the array of elements # Sometimes get stale reference if I hold on to the array of elements
...@@ -42,3 +97,26 @@ class AdvancedSettingsPage(CoursePage): ...@@ -42,3 +97,26 @@ class AdvancedSettingsPage(CoursePage):
def get(self, key): def get(self, key):
index = self._get_index_of(key) index = self._get_index_of(key)
return get_codemirror_value(self, index) 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
"""
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"',
}
)
...@@ -7,4 +7,4 @@ ...@@ -7,4 +7,4 @@
<div>i am a dummy asset file</div> <div>i am a dummy asset file</div>
</body> </body>
</html> </html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment