Commit 69221521 by Don Mitchell

Merge pull request #1653 from MITx/feature/studio/advanced-settings-revamp-merge

Advanced Settings changes.
parents f155bd09 eea59005
...@@ -2,53 +2,41 @@ Feature: Advanced (manual) course policy ...@@ -2,53 +2,41 @@ Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key/value pairs I want to be able to manually enter JSON key/value pairs
Scenario: A course author sees only display_name on a newly created course Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio Given I have opened a new course in Studio
When I select the Advanced Settings When I select the Advanced Settings
Then I see only the display name Then I see default advanced settings
@skip-phantom Scenario: Add new entries, and they appear alphabetically after save
Scenario: Test if there are no policy settings without existing UI controls
Given I am on the Advanced Course Settings page in Studio
When I delete the display name
Then there are no advanced policy settings
And I reload the page
Then there are no advanced policy settings
@skip-phantom
Scenario: Test cancel editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
And I press the "Cancel" notification button
Then the policy key name is unchanged
Scenario: Test editing key name
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key Then the settings are alphabetized
And I press the "Save" notification button
Then the policy key name is changed
Scenario: Test cancel editing key value Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
And I press the "Cancel" notification button And I press the "Cancel" notification button
Then the policy key value is unchanged Then the policy key value is unchanged
And I reload the page
Then the policy key value is unchanged
@skip-phantom
Scenario: Test editing key value Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
And I press the "Save" notification button And I press the "Save" notification button
Then the policy key value is changed Then the policy key value is changed
Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
When I create New Entries
Then they are alphabetized
And I reload the page And I reload the page
Then they are alphabetized Then the policy key value is changed
Scenario: Test how multi-line input appears Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a JSON object When I create a JSON object as a value
Then it is displayed as formatted Then it is displayed as formatted
And I reload the page
Then it is displayed as formatted
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes
Then it is displayed as a string
And I reload the page
Then it is displayed as a string
from lettuce import world, step from lettuce import world, step
from common import * from common import *
import time import time
from terrain.steps import reload_the_page
from selenium.common.exceptions import WebDriverException from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
...@@ -11,6 +12,10 @@ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdrive ...@@ -11,6 +12,10 @@ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdrive
""" """
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
KEY_CSS = '.key input.policy-key'
VALUE_CSS = 'textarea.json'
DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS #################### ############### ACTIONS ####################
@step('I select the Advanced Settings$') @step('I select the Advanced Settings$')
...@@ -20,7 +25,6 @@ def i_select_advanced_settings(step): ...@@ -20,7 +25,6 @@ def i_select_advanced_settings(step):
css_click(expand_icon_css) css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a' link_css = 'li.nav-course-settings-advanced a'
css_click(link_css) css_click(link_css)
# world.browser.click_link_by_text('Advanced Settings')
@step('I am on the Advanced Course Settings page in Studio$') @step('I am on the Advanced Course Settings page in Studio$')
...@@ -29,35 +33,27 @@ def i_am_on_advanced_course_settings(step): ...@@ -29,35 +33,27 @@ def i_am_on_advanced_course_settings(step):
step.given('I select the Advanced Settings') step.given('I select the Advanced Settings')
# TODO: this is copied from terrain's step.py. Need to figure out how to share that code.
@step('I reload the page$')
def reload_the_page(step):
world.browser.reload()
@step(u'I edit the name of a policy key$')
def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e.type('_new')
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
def is_visible(driver): def is_visible(driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
def is_invisible(driver):
return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,)) # def is_invisible(driver):
# return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower() css = 'a.%s-button' % name.lower()
wait_for(is_visible) wait_for(is_visible)
time.sleep(float(1))
css_click_at(css)
# is_invisible is not returning a boolean, not working
# try:
# css_click_at(css)
# wait_for(is_invisible)
# except WebDriverException, e:
# css_click_at(css)
# wait_for(is_invisible)
try:
css_click_at(css)
wait_for(is_invisible)
except WebDriverException, e:
css_click_at(css)
wait_for(is_invisible)
@step(u'I edit the value of a policy key$') @step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step): def edit_the_value_of_a_policy_key(step):
...@@ -65,133 +61,86 @@ def edit_the_value_of_a_policy_key(step): ...@@ -65,133 +61,86 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :) area, so cheat and do it from the policy key field :)
""" """
policy_key_css = 'input.policy-key' e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e = css_find(policy_key_css).first
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X') e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@step('I delete the display name$') @step('I create a JSON object as a value$')
def delete_the_display_name(step):
delete_entry(0)
click_save()
@step('create New Entries$')
def create_new_entries(step):
create_entry("z", "apple")
create_entry("a", "zebra")
click_save()
@step('I create a JSON object$')
def create_JSON_object(step): def create_JSON_object(step):
create_entry("json", '{"key": "value", "key_2": "value_2"}') change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
click_save()
############### RESULTS #################### @step('I create a non-JSON value not in quotes$')
@step('I see only the display name$') def create_value_not_in_quotes(step):
def i_see_only_display_name(step): change_display_name_value(step, 'quote me')
assert_policy_entries(["display_name"], ['"Robot Super Course"'])
@step('there are no advanced policy settings$') ############### RESULTS ####################
def no_policy_settings(step): @step('I see default advanced settings$')
keys_css = 'input.policy-key' def i_see_default_advanced_settings(step):
val_css = 'textarea.json' # Test only a few of the existing properties (there are around 34 of them)
k = world.browser.is_element_not_present_by_css(keys_css, 5) assert_policy_entries(
v = world.browser.is_element_not_present_by_css(val_css, 5) ["advanced_modules", DISPLAY_NAME_KEY, "show_calculator"], ["[]", DISPLAY_NAME_VALUE, "false"])
assert_true(k)
assert_true(v)
@step('they are alphabetized$') @step('the settings are alphabetized$')
def they_are_alphabetized(step): def they_are_alphabetized(step):
assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"']) key_elements = css_find(KEY_CSS)
all_keys = []
for key in key_elements:
all_keys.append(key.value)
assert_equal(sorted(all_keys), all_keys, "policy keys were not sorted")
@step('it is displayed as formatted$') @step('it is displayed as formatted$')
def it_is_formatted(step): def it_is_formatted(step):
assert_policy_entries(["display_name", "json"], ['"Robot Super Course"', '{\n "key": "value",\n "key_2": "value_2"\n}']) assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
@step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key'
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name')
@step('it is displayed as a string')
@step(u'the policy key name is changed$') def it_is_formatted(step):
def the_policy_key_name_is_changed(step): assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
policy_key_css = 'input.policy-key'
val = css_find(policy_key_css).first.value
assert_equal(val, 'display_name_new')
@step(u'the policy key value is unchanged$') @step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step): def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' assert_equal(get_display_name_value(), DISPLAY_NAME_VALUE)
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course"')
@step(u'the policy key value is changed$') @step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step): def the_policy_key_value_is_changed(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' assert_equal(get_display_name_value(), '"Robot Super Course X"')
val = css_find(policy_value_css).first.value
assert_equal(val, '"Robot Super Course X"')
############# HELPERS ############### ############# HELPERS ###############
def create_entry(key, value):
# Scroll down the page so the button is visible
world.scroll_to_bottom()
css_click_at('a.new-advanced-policy-item', 10, 10)
new_key_css = 'div#__new_advanced_key__ input'
new_key_element = css_find(new_key_css).first
new_key_element.fill(key)
# For some reason have to get the instance for each command
# (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium fill does not remove existing text
new_value_css = 'div.CodeMirror textarea'
css_find(new_value_css).last.fill("")
css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
css_find(new_value_css).last.fill(value)
# Add in a TAB key press because intermittently on ubuntu the
# last character of "value" above was not getting typed in
css_find(new_value_css).last._element.send_keys(Keys.TAB)
def delete_entry(index):
"""
Delete the nth entry where index is 0-based
"""
css = 'a.delete-button'
assert_true(world.browser.is_element_present_by_css(css, 5))
delete_buttons = css_find(css)
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): def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input.policy-key', expected_keys) for counter in range(len(expected_keys)):
assert_entries('textarea.json', expected_values) index = get_index_of(expected_keys[counter])
assert_false(index == -1, "Could not find key: " + expected_keys[counter])
assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect")
def assert_entries(css, expected_values): def get_index_of(expected_key):
webElements = css_find(css) for counter in range(len(css_find(KEY_CSS))):
assert_equal(len(expected_values), len(webElements)) # 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 key = css_find(KEY_CSS)[counter].value
for counter in range(len(expected_values)): if key == expected_key:
assert_equal(expected_values[counter], css_find(css)[counter].value) return counter
return -1
def click_save():
css = "a.save-button" def get_display_name_value():
css_click_at(css) index = get_index_of(DISPLAY_NAME_KEY)
return css_find(VALUE_CSS)[index].value
def fill_last_field(value): def change_display_name_value(step, new_value):
newValue = css_find('#__new_advanced_key__ input').first e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
newValue.fill(value) display_name = get_display_name_value()
for count in range(len(display_name)):
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
press_the_notification_button(step, "Save")
\ No newline at end of file
...@@ -1228,7 +1228,6 @@ def course_config_advanced_page(request, org, course, name): ...@@ -1228,7 +1228,6 @@ def course_config_advanced_page(request, org, course, name):
return render_to_response('settings_advanced.html', { return render_to_response('settings_advanced.html', {
'context_course': course_module, 'context_course': course_module,
'course_location' : location, 'course_location' : location,
'advanced_blacklist' : json.dumps(CourseMetadata.FILTERED_LIST),
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)), 'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
}) })
......
...@@ -10,8 +10,7 @@ class CourseMetadata(object): ...@@ -10,8 +10,7 @@ class CourseMetadata(object):
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones. For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata. The objects have no predefined attrs but instead are obj encodings of the editable metadata.
''' '''
# __new_advanced_key__ is used by client not server; so, could argue against it being here FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
......
<li class="field-group course-advanced-policy-list-item"> <li class="field-group course-advanced-policy-list-item">
<div class="field text key" id="<%= (_.isEmpty(key) ? '__new_advanced_key__' : key) %>"> <div class="field is-not-editable text key" id="<%= key %>">
<label for="<%= keyUniqueId %>">Policy Key:</label> <label for="<%= keyUniqueId %>">Policy Key:</label>
<input type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" /> <input readonly title="This field is disabled: policy keys cannot be edited." type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
</div> </div>
<div class="field text value"> <div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label> <label for="<%= valueUniqueId %>">Policy Value:</label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea> <textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
</div> </div>
<div class="actions">
<a href="#" class="button delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
</div>
</li> </li>
\ No newline at end of file
if (!CMS.Models['Settings']) CMS.Models.Settings = {}; if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({ 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: { defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server) // the properties are whatever the user types in (in addition to whatever comes originally from the server)
}, },
// which keys to send as the deleted keys on next save // which keys to send as the deleted keys on next save
deleteKeys : [], deleteKeys : [],
blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
validate: function (attrs) { validate: function (attrs) {
var errors = {}; // Keys can no longer be edited. We are currently not validating values.
for (var key in attrs) {
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 can be edited on another screen";
}
}
if (!_.isEmpty(errors)) return errors;
}, },
save : function (attrs, options) { save : function (attrs, options) {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return; if (typeof window.templateLoader == 'function') return;
var templateLoader = { var templateLoader = {
templateVersion: "0.0.15", templateVersion: "0.0.16",
templates: {}, templates: {},
loadRemoteTemplate: function(templateName, filename, callback) { loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) { if (!this.templates[templateName]) {
......
...@@ -6,14 +6,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -6,14 +6,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.Advanced // Model class is CMS.Models.Settings.Advanced
events : { events : {
'click .delete-button' : "deleteEntry",
'click .new-button' : "addEntry",
// update model on changes
'change .policy-key' : "updateKey",
// keypress to catch alpha keys and backspace/delete on some browsers
'keypress .policy-key' : "showSaveCancelButtons",
// keyup to catch backspace/delete reliably
'keyup .policy-key' : "showSaveCancelButtons",
'focus :input' : "focusInput", 'focus :input' : "focusInput",
'blur :input' : "blurInput" 'blur :input' : "blurInput"
// TODO enable/disable save based on validation (currently enabled whenever there are changes) // TODO enable/disable save based on validation (currently enabled whenever there are changes)
...@@ -95,16 +87,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -95,16 +87,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
mirror.setValue(stringValue); mirror.setValue(stringValue);
} catch(quotedE) { } catch(quotedE) {
// TODO: validation error // 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); // console.log(quotedE);
JSONValue = undefined; JSONValue = undefined;
} }
} }
else {
// TODO: validation error
console.log("Error with JSON, but will not convert to String.");
console.log(e);
}
} }
if (JSONValue !== undefined) { if (JSONValue !== undefined) {
self.clearValidationErrors(); self.clearValidationErrors();
...@@ -113,7 +100,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -113,7 +100,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
} }
}); });
}, },
showMessage: function (type) { showMessage: function (type) {
this.$el.find(".message-status").removeClass("is-shown"); this.$el.find(".message-status").removeClass("is-shown");
if (type) { if (type) {
...@@ -128,56 +114,19 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -128,56 +114,19 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
else { else {
// This is the case of the page first rendering, or when Cancel is pressed. // This is the case of the page first rendering, or when Cancel is pressed.
this.hideSaveCancelButtons(); this.hideSaveCancelButtons();
this.toggleNewButton(true);
} }
}, },
showSaveCancelButtons: function(event) { showSaveCancelButtons: function(event) {
if (!this.buttonsVisible) { if (!this.buttonsVisible) {
if (event && (event.type === 'keypress' || event.type === 'keyup')) {
// check whether it's really an altering event: note, String.fromCharCode(keyCode) will
// give positive values for control/command/option-letter combos; so, don't use it
if (!((event.charCode && String.fromCharCode(event.charCode) !== "") ||
// 8 = backspace, 46 = delete
event.keyCode === 8 || event.keyCode === 46)) return;
}
this.$el.find(".message-status").removeClass("is-shown"); this.$el.find(".message-status").removeClass("is-shown");
$('.wrapper-notification').addClass('is-shown'); $('.wrapper-notification').addClass('is-shown');
this.buttonsVisible = true; this.buttonsVisible = true;
} }
}, },
hideSaveCancelButtons: function() { hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown'); $('.wrapper-notification').removeClass('is-shown');
this.buttonsVisible = false; this.buttonsVisible = false;
}, },
toggleNewButton: function (enable) {
var newButton = this.$el.find(".new-button");
if (enable) {
newButton.removeClass('disabled');
}
else {
newButton.addClass('disabled');
}
},
deleteEntry : function(event) {
event.preventDefault();
// find out which entry
var li$ = $(event.currentTarget).closest('li');
// Not data b/c the validation view uses it for a selector
var key = $('.key', li$).attr('id');
delete this.selectorToField[this.fieldToSelectorMap[key]];
delete this.fieldToSelectorMap[key];
if (key !== this.model.new_key) {
this.model.deleteKeys.push(key);
this.model.unset(key);
}
li$.remove();
this.showSaveCancelButtons();
},
saveView : function(event) { saveView : function(event) {
// TODO one last verification scan: // TODO one last verification scan:
// call validateKey on each to ensure proper format // call validateKey on each to ensure proper format
...@@ -201,102 +150,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -201,102 +150,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
error : CMS.ServerError error : CMS.ServerError
}); });
}, },
addEntry : function() {
var listEle$ = this.$el.find('.course-advanced-policy-list');
var newEle = this.renderTemplate("", "");
listEle$.append(newEle);
// need to re-find b/c replaceWith seems to copy rather than use the specific ele instance
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);
},
updateKey : function(event) {
var parentElement = $(event.currentTarget).closest('.key');
// 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 = parentElement.attr('id');
// 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("invalid", 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.model.new_key ? '' : this.model.get(oldKey));
var validation = this.model.validate(newEntryModel);
if (validation) {
if (_.has(validation, newKey)) {
// swap to the key which the map knows about
validation[oldKey] = validation[newKey];
}
this.model.trigger("invalid", this.model, validation);
// abandon update
return;
}
// Now safe to actually do the update
this.model.set(newEntryModel);
// update maps
var selector = this.fieldToSelectorMap[oldKey];
this.selectorToField[selector] = newKey;
this.fieldToSelectorMap[newKey] = selector;
delete this.fieldToSelectorMap[oldKey];
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) ;
}
else {
// id for the new entry will now be the key value. Enable new entry button.
this.toggleNewButton(true);
}
// check for newkey being the name of one which was previously deleted in this session
var wasDeleting = this.model.deleteKeys.indexOf(newKey);
if (wasDeleting >= 0) {
this.model.deleteKeys.splice(wasDeleting, 1);
}
// Update the ID to the new value.
parentElement.attr('id', newKey);
}
},
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 (note some checking for spaces currently done in model's
// validate method.
return true;
},
renderTemplate: function (key, value) { renderTemplate: function (key, value) {
var newKeyId = _.uniqueId('policy_key_'), var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4), newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')}); keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
this.fieldToSelectorMap[(_.isEmpty(key) ? this.model.new_key : key)] = newKeyId; this.fieldToSelectorMap[key] = newKeyId;
this.selectorToField[newKeyId] = (_.isEmpty(key) ? this.model.new_key : key); this.selectorToField[newKeyId] = key;
return newEle; return newEle;
}, },
focusInput : function(event) { focusInput : function(event) {
$(event.target).prev().addClass("is-focused"); $(event.target).prev().addClass("is-focused");
}, },
......
...@@ -405,6 +405,21 @@ textarea.text { ...@@ -405,6 +405,21 @@ textarea.text {
@include linear-gradient($paleYellow, tint($paleYellow, 90%)); @include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0; outline: 0;
} }
&[disabled] {
border-color: $gray-l4;
color: $gray-l2;
}
&[readonly] {
border-color: $gray-l4;
color: $gray-l1;
&:focus {
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
outline: 0;
}
}
} }
// forms - specific // forms - specific
......
...@@ -239,13 +239,9 @@ body.course.settings { ...@@ -239,13 +239,9 @@ body.course.settings {
// not editable fields // not editable fields
.field.is-not-editable { .field.is-not-editable {
label, .label { & label.is-focused {
color: $gray-l3; color: $gray-d2;
}
input {
opacity: 0.25;
} }
} }
......
...@@ -70,17 +70,17 @@ from contentstore import utils ...@@ -70,17 +70,17 @@ from contentstore import utils
<ol class="list-input"> <ol class="list-input">
<li class="field text is-not-editable" id="field-course-organization"> <li class="field text is-not-editable" id="field-course-organization">
<label for="course-organization">Organization</label> <label for="course-organization">Organization</label>
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled" /> <input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
</li> </li>
<li class="field text is-not-editable" id="field-course-number"> <li class="field text is-not-editable" id="field-course-number">
<label for="course-number">Course Number</label> <label for="course-number">Course Number</label>
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled"> <input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly>
</li> </li>
<li class="field text is-not-editable" id="field-course-name"> <li class="field text is-not-editable" id="field-course-name">
<label for="course-name">Course Name</label> <label for="course-name">Course Name</label>
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled" /> <input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
</li> </li>
</ol> </ol>
<span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span> <span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>
......
...@@ -21,7 +21,6 @@ $(document).ready(function () { ...@@ -21,7 +21,6 @@ $(document).ready(function () {
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern // proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
var advancedModel = new CMS.Models.Settings.Advanced(${advanced_dict | n}, {parse: true}); var advancedModel = new CMS.Models.Settings.Advanced(${advanced_dict | n}, {parse: true});
advancedModel.blacklistKeys = ${advanced_blacklist | n};
advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}"; advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var editor = new CMS.Views.Settings.Advanced({ var editor = new CMS.Views.Settings.Advanced({
...@@ -61,18 +60,11 @@ editor.render(); ...@@ -61,18 +60,11 @@ editor.render();
<span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span> <span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span>
</header> </header>
<p class="instructions"><strong>Warning</strong>: Add only manual policy data that you are familiar <p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p>
with.</p>
<ul class="list-input course-advanced-policy-list enum"> <ul class="list-input course-advanced-policy-list enum">
</ul> </ul>
<div class="actions">
<a href="#" class="button new-button new-advanced-policy-item add-policy-data">
<span class="plus-icon white"></span>New Manual Policy
</a>
</div>
</section> </section>
</form> </form>
</article> </article>
...@@ -80,9 +72,9 @@ editor.render(); ...@@ -80,9 +72,9 @@ editor.render();
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <div class="bit">
<h3 class="title-3">How will these settings be used?</h3> <h3 class="title-3">How will these settings be used?</h3>
<p>Manual policies are JSON-based key and value pairs that allow you add additional settings which edX Studio will use when generating your course.</p> <p>Manual policies are JSON-based key and value pairs that give you control over specific course settings that edX Studio will use when displaying and running your course.</p>
<p>Any policies you define here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not add policies that you are unfamiliar with (both their purpose and their syntax).</p> <p>Any policies you modify here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not edit policies that you are unfamiliar with (both their purpose and their syntax).</p>
</div> </div>
<div class="bit"> <div class="bit">
...@@ -110,7 +102,7 @@ editor.render(); ...@@ -110,7 +102,7 @@ editor.render();
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i> <i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<p><strong>Note: </strong>Your changes will not take effect until you <strong>save your <p><strong>Note: </strong>Your changes will not take effect until you <strong>save your
progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p> progress</strong>. Take care with policy value formatting, as validation is <strong>not implemented</strong>.</p>
</div> </div>
<div class="actions"> <div class="actions">
......
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