From 8acbeec3add4e65149f2ccb9dbca44bbc4e4dfc0 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 20 Feb 2013 12:23:19 -0500 Subject: [PATCH 1/2] Comment out validation, update warning message. --- cms/static/js/models/settings/advanced.js | 8 ++- cms/static/js/views/settings/advanced_view.js | 66 +++++++++++-------- cms/templates/settings_advanced.html | 2 +- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/cms/static/js/models/settings/advanced.js b/cms/static/js/models/settings/advanced.js index 052f8c9bd6..754be11749 100644 --- a/cms/static/js/models/settings/advanced.js +++ b/cms/static/js/models/settings/advanced.js @@ -1,6 +1,9 @@ if (!CMS.Models['Settings']) CMS.Models.Settings = {}; CMS.Models.Settings.Advanced = Backbone.Model.extend({ + // the key for a newly added policy-- before the user has entered a key value + new_key : "__new_advanced_key__", + defaults: { // the properties are whatever the user types in (in addition to whatever comes originally from the server) }, @@ -11,7 +14,10 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({ validate: function (attrs) { var errors = {}; for (var key in attrs) { - if (_.contains(this.blacklistKeys, key)) { + if (key === this.new_key || _.isEmpty(key)) { + errors[key] = "A key must be entered."; + } + else if (_.contains(this.blacklistKeys, key)) { errors[key] = key + " is a reserved keyword or has another editor"; } } diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 04dd8ad53d..2ae304e502 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -1,8 +1,6 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {}; CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ - // the key for a newly added policy-- before the user has entered a key value - new_key : "__new_advanced_key__", error_saving : "error_saving", successful_changes: "successful_changes", @@ -54,6 +52,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ return this; }, attachJSONEditor : function (textarea) { + // Since we are allowing duplicate keys at the moment, it is possible that we will try to attach + // JSON Editor to a value that already has one. Therefore only attach if no CodeMirror peer exists. + var siblings = $(textarea).siblings(); + if (siblings.length >= 1 && $(siblings[0]).hasClass('CodeMirror')) { + return; + } + var self = this; CodeMirror.fromTextArea(textarea, { mode: "application/json", lineNumbers: false, lineWrapping: false, @@ -79,18 +84,20 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ mirror.setValue(stringValue); } catch(quotedE) { // TODO: validation error - console.log("Error with JSON, even after converting to String.") + console.log("Error with JSON, even after converting to String."); console.log(quotedE); JSONValue = undefined; } } else { // TODO: validation error - console.log("Error with JSON, but will not convert to String.") + console.log("Error with JSON, but will not convert to String."); console.log(e); } } if (JSONValue !== undefined) { + // Is it OK to clear all validation errors? If we don't we get problems with errors overlaying. + self.clearValidationErrors(); self.model.set(key, JSONValue, {validate: true}); } } @@ -146,7 +153,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ var key = $('.key', li$).attr('id'); delete this.fieldToSelectorMap[key]; - if (key !== this.new_key) { + if (key !== this.model.new_key) { this.model.deleteKeys.push(key); this.model.unset(key); } @@ -182,9 +189,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ listEle$.append(newEle); // disable the value entry until there's an acceptable key $(newEle).find('.course-advanced-policy-value').addClass('disabled'); - this.fieldToSelectorMap[this.new_key] = this.new_key; + this.fieldToSelectorMap[this.model.new_key] = this.model.new_key; // need to re-find b/c replaceWith seems to copy rather than use the specific ele instance - var policyValueDivs = this.$el.find('#' + this.new_key).closest('li').find('.json'); + var policyValueDivs = this.$el.find('#' + this.model.new_key).closest('li').find('.json'); // only 1 but hey, let's take advantage of the context mechanism _.each(policyValueDivs, this.attachJSONEditor, this); this.toggleNewButton(false); @@ -194,26 +201,29 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ // old key: either the key as in the model or new_key. // That is, it doesn't change as the val changes until val is accepted. var oldKey = $(event.currentTarget).closest('.key').attr('id'); - var newKey = $(event.currentTarget).val(); + // TODO: validation of keys with spaces. For now at least trim strings to remove initial and + // trailing whitespace + var newKey = $.trim($(event.currentTarget).val()); if (oldKey !== newKey) { // TODO: is it OK to erase other validation messages? this.clearValidationErrors(); if (!this.validateKey(oldKey, newKey)) return; - if (this.model.has(newKey)) { - var error = {}; - error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.'; - error[newKey] = "You tried to enter a duplicate of this key."; - this.model.trigger("error", this.model, error); - return false; - } +// TODO: re-enable validation +// if (this.model.has(newKey)) { +// var error = {}; +// error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.'; +// error[newKey] = "You tried to enter a duplicate of this key."; +// this.model.trigger("error", this.model, error); +// return false; +// } // explicitly call validate to determine whether to proceed (relying on triggered error means putting continuation in the success // method which is uglier I think?) var newEntryModel = {}; // set the new key's value to the old one's - newEntryModel[newKey] = (oldKey === this.new_key ? '' : this.model.get(oldKey)); + newEntryModel[newKey] = (oldKey === this.model.new_key ? '' : this.model.get(oldKey)); var validation = this.model.validate(newEntryModel); if (validation) { @@ -231,7 +241,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ delete this.fieldToSelectorMap[oldKey]; - if (oldKey !== this.new_key) { + if (oldKey !== this.model.new_key) { // mark the old key for deletion and delete from field maps this.model.deleteKeys.push(oldKey); this.model.unset(oldKey) ; @@ -252,7 +262,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ $(event.currentTarget).closest('li').replaceWith(newEle); // need to re-find b/c replaceWith seems to copy rather than use the specific ele instance var policyValueDivs = this.$el.find('#' + newKey).closest('li').find('.json'); - // only 1 but hey, let's take advantage of the context mechanism + + // Because we are not dis-allowing duplicate key definitions, we may get back more than one + // div. But attachJSONEditor will only attach editor if it is not already defined. _.each(policyValueDivs, this.attachJSONEditor, this); this.fieldToSelectorMap[newKey] = newKey; @@ -260,13 +272,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ }, validateKey : function(oldKey, newKey) { // model validation can't handle malformed keys nor notice if 2 fields have same key; so, need to add that chk here - // TODO ensure there's no spaces or illegal chars - if (_.isEmpty(newKey)) { - var error = {}; - error[oldKey] = "Key cannot be an empty string"; - this.model.trigger("error", this.model, error); - return false; - } - else return true; + // TODO ensure there's no spaces or illegal chars (note some checking for spaces currently done in model's + // validate method. +// if (_.isEmpty(newKey)) { +// var error = {}; +// error[oldKey] = "Key cannot be an empty string"; +// this.model.trigger("error", this.model, error); +// return false; +// } +// else return true; + return true; } }); \ No newline at end of file diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index 5fe1b21a9b..d98e694b8d 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -93,7 +93,7 @@ from contentstore import utils

Note: Your changes will not take effect until you save your - progress.

+ progress. Be careful with syntax as validation is not implemented.

From 05f32f4bc937c892b1d763a84092ab75baaccbfa Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 20 Feb 2013 14:30:39 -0500 Subject: [PATCH 2/2] Update tests for the advanced policy settings feature. Includes adding a method for css_click_at. --- .../features/advanced-settings.feature | 22 ++++++------ .../features/advanced-settings.py | 33 ++++++++++------- .../contentstore/features/common.py | 35 +++++++++++++++++-- common/djangoapps/terrain/steps.py | 5 +++ 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index a0ad8004cc..4790c2eef7 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -7,18 +7,18 @@ Feature: Advanced (manual) course policy When I select the Advanced Settings Then I see only the display name - Scenario: A course author sees something sensible if there are no policy settings without existing UI controls + Scenario: Test if there are no policy settings without existing UI controls Given I have opened a new course in Studio When I select the Advanced Settings And I delete the display name - Then There are no advanced policy settings - And I refresh and select the Advanced Settings - Then There are no advanced policy settings + Then there are no advanced policy settings + And I reload the page + Then there are no advanced policy settings - Scenario: A course author can add new entries, and they appear alphabetically after save - Given I have opened a new course in Studio - When I select the Advanced Settings - And Create New Entries - Then They are alphabetized - And I refresh and select the Advanced Settings - Then They are alphabetized \ No newline at end of file + Scenario: Add new entries, and they appear alphabetically after save + Given I have opened a new course in Studio + When I select the Advanced Settings + And I create New Entries + Then they are alphabetized + And I reload the page + Then they are alphabetized \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 19460220d5..5356a96739 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -11,41 +11,43 @@ from selenium.webdriver.common.keys import Keys ############### ACTIONS #################### @step('I select the Advanced Settings$') def i_select_advanced_settings(step): - link_css = 'a#settings-tab' - css_click(link_css) - link_css = "[data-section='advanced']" + expand_icon_css = 'li.nav-course-settings i.icon-expand' + if world.browser.is_element_present_by_css(expand_icon_css): + css_click(expand_icon_css) + link_css = 'li.nav-course-settings-advanced a' css_click(link_css) -@step('I refresh and select the Advanced Settings$') -def refresh_and_select_advanced_settings(step): - reload() - i_select_advanced_settings(step) @step('I see only the display name$') def i_see_only_display_name(step): assert_policy_entries(["display_name"], ['"Robot Super Course"']) + @step('I delete the display name') def i_delete_the_display_name(step): delete_entry(0) click_save() -@step("There are no advanced policy settings$") + +@step('there are no advanced policy settings$') def no_policy_settings(step): assert_policy_entries([], []) -@step("Create New Entries") + +@step('create New Entries$') def create_new_entries(step): create_entry("z", "apple") create_entry("a", "zebra") click_save() -@step("They are alphabetized") + +@step('they are alphabetized$') def they_are_alphabetized(step): assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"']) + def create_entry(key, value): - css_click(".new-advanced-policy-item") + css_click_at('a.new-advanced-policy-item') newKey = css_find('#__new_advanced_key__ input').first newKey.fill(key) # For some reason have to get the instance for each command (get error that it is no longer attached to the DOM) @@ -54,6 +56,7 @@ def create_entry(key, value): css_find('.CodeMirror textarea').last._element.send_keys(Keys.ARROW_LEFT) css_find('.CodeMirror textarea').last.fill(value) + def delete_entry(index): """ index is 0-based """ @@ -63,17 +66,20 @@ def delete_entry(index): assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index)) delete_buttons[index].click() + def assert_policy_entries(expected_keys, expected_values): assert_entries('.key input', expected_keys) assert_entries('.json', expected_values) + def assert_entries(css, expected_values): webElements = css_find(css) - assert_equal(len(expected_values),len(webElements)) + assert_equal(len(expected_values), len(webElements)) # Sometimes get stale reference if I hold on to the array of elements for counter in range(len(expected_values)): assert_equal(expected_values[counter], css_find(css)[counter].value) + def click_save(): css = ".save-button" def is_shown(driver): @@ -85,6 +91,7 @@ def click_save(): wait_for(is_shown) css_click(css) + def fill_last_field(value): newValue = css_find('#__new_advanced_key__ input').first - newValue.fill(value) \ No newline at end of file + newValue.fill(value) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index d0d007b231..e611d81b4f 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -10,6 +10,7 @@ from logging import getLogger logger = getLogger(__name__) ########### STEP HELPERS ############## + @step('I (?:visit|access|open) the Studio homepage$') def i_visit_the_studio_homepage(step): # To make this go to port 8001, put @@ -19,14 +20,17 @@ def i_visit_the_studio_homepage(step): signin_css = 'a.action-signin' assert world.browser.is_element_present_by_css(signin_css, 10) + @step('I am logged into Studio$') def i_am_logged_into_studio(step): log_into_studio() + @step('I confirm the alert$') def i_confirm_with_ok(step): world.browser.get_alert().accept() + @step(u'I press the "([^"]*)" delete icon$') def i_press_the_category_delete_icon(step, category): if category == 'section': @@ -37,6 +41,7 @@ def i_press_the_category_delete_icon(step, category): assert False, 'Invalid category: %s' % category css_click(css) + @step('I have opened a new course in Studio$') def i_have_opened_a_new_course(step): clear_courses() @@ -44,13 +49,14 @@ def i_have_opened_a_new_course(step): create_a_course() ####### HELPER FUNCTIONS ############## + def create_studio_user( uname='robot', email='robot+studio@edx.org', password='test', - is_staff=False): + is_staff=False): studio_user = UserFactory.build( - username=uname, + username=uname, email=email, password=password, is_staff=is_staff) @@ -63,6 +69,7 @@ def create_studio_user( user_profile = UserProfileFactory(user=studio_user) + def flush_xmodule_store(): # Flush and initialize the module store # It needs the templates because it creates new records @@ -75,32 +82,53 @@ def flush_xmodule_store(): xmodule.modulestore.django.modulestore().collection.drop() xmodule.templates.update_templates() + def assert_css_with_text(css, text): assert_true(world.browser.is_element_present_by_css(css, 5)) assert_equal(world.browser.find_by_css(css).text, text) + def css_click(css): assert_true(world.browser.is_element_present_by_css(css, 5)) world.browser.find_by_css(css).first.click() + +def css_click_at(css, x=10, y=10): + ''' + A method to click at x,y coordinates of the element + rather than in the center of the element + ''' + assert_true(world.browser.is_element_present_by_css(css, 5)) + e = world.browser.find_by_css(css).first + e.action_chains.move_to_element_with_offset(e._element, x, y) + e.action_chains.click() + e.action_chains.perform() + + def css_fill(css, value): world.browser.find_by_css(css).first.fill(value) + def css_find(css): return world.browser.find_by_css(css) + def wait_for(func): WebDriverWait(world.browser.driver, 10).until(func) + def id_find(id): return world.browser.find_by_id(id) + def reload(): return world.browser.reload() + def clear_courses(): flush_xmodule_store() + def fill_in_course_info( name='Robot Super Course', org='MITx', @@ -109,6 +137,7 @@ def fill_in_course_info( css_fill('.new-course-org', org) css_fill('.new-course-number', num) + def log_into_studio( uname='robot', email='robot+studio@edx.org', @@ -130,6 +159,7 @@ def log_into_studio( assert_true(world.browser.is_element_present_by_css('.new-course-button', 5)) + def create_a_course(): css_click('a.new-course-button') fill_in_course_info() @@ -137,6 +167,7 @@ def create_a_course(): course_title_css = 'span.course-title' assert_true(world.browser.is_element_present_by_css(course_title_css, 5)) + def add_section(name='My Section'): link_css = 'a.new-courseware-section-button' css_click(link_css) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 6b2a813d8d..a69f8dc7a2 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -21,6 +21,11 @@ def wait(step, seconds): time.sleep(float(seconds)) +@step('I reload the page$') +def reload_the_page(step): + world.browser.reload() + + @step('I (?:visit|access|open) the homepage$') def i_visit_the_homepage(step): world.browser.visit(django_url('/'))