Commit 8f16d639 by Don Mitchell

CRUD on policy fields w/ some validation

parent 07f87d47
......@@ -1109,6 +1109,8 @@ def get_course_settings(request, org, course, name):
return render_to_response('settings.html', {
'active_tab': 'settings',
'context_course': course_module,
'advanced_blacklist' : json.dumps(CourseMetadata.FILTERED_LIST),
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
})
......@@ -1133,6 +1135,9 @@ def course_settings_updates(request, org, course, name, section):
manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
elif section == 'advanced':
# not implemented b/c it assumes prefetched and then everything thru course_edit_metadata
return
else: return
if request.method == 'GET':
......@@ -1194,14 +1199,10 @@ def course_edit_metadata(request, org, course, name):
editable = CourseMetadata.fetch(location)
return render_to_response('course_info.html', {
'active_tab': 'settings',
'editable_metadata': editable,
'url_base' : "/" + org + "/" + course + "/",
'blacklist_keys' : CourseMetadata.FILTERED_LIST
})
# for now defer to settings general until we split the divs out into separate pages
return get_course_settings(request, org, course, name)
@expect_json
## NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
def course_metadata_rest_access(request, org, course, name):
......@@ -1225,10 +1226,11 @@ def course_metadata_rest_access(request, org, course, name):
if request.method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, request.POST)), mimetype="application/json")
elif real_method == 'DELETE':
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json")
elif request.method == 'POST':
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, request.POST)), mimetype="application/json")
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json")
@login_required
......
......@@ -7,8 +7,8 @@ class CourseMetadata(object):
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.
'''
FILTERED_LIST = ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
@classmethod
def fetch(cls, course_location):
......@@ -57,14 +57,10 @@ class CourseMetadata(object):
'''
descriptor = get_modulestore(course_location).get_item(course_location)
if isinstance(payload, list):
for key in payload:
if key in descriptor.metadata:
del descriptor.metadata[key]
else:
if payload in descriptor.metadata:
del descriptor.metadata[payload]
for key in payload['deleteKeys']:
if key in descriptor.metadata:
del descriptor.metadata[key]
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
return cls.fetch(course_location)
<li class="input multi course-advanced-policy-list-item">
<div class="row">
<div class="key" id="<%= (_.isEmpty(key) ? '__new_advanced_key__' : key) %>">
<label for="course-advanced-policy-key">Policy Key:</label>
<div class="field">
<input type="text" class="short" id="course-advanced-policy-key" 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="value">
<label for="course-advanced-policy-value">Policy Value:</label>
<div class="field">
<textarea class="ace text" id="course-advanced-policy-value"><%= value %></textarea>
</div>
</div>
</div> <a href="#" class="delete-button standard remove-item advanced-policy-data">
<span class="delete-icon"></span>Delete</a>
</li>
\ No newline at end of file
......@@ -2,13 +2,211 @@ if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({
defaults: {
// the properties are whatever the user types in
},
// which keys to send as the deleted keys on next save
deleteKeys : [],
blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
initialize: function() {
console.log('in initialize');
var editor = ace.edit('course-advanced-policy-1-value');
editor.setTheme("ace/theme/monokai");
editor.getSession().setMode("ace/mode/javascript");
},
validate: function(attrs) {
var errors = {};
for (key in attrs) {
if (_.contains(this.blacklistKeys, key)) {
errors[key] = key + " is a reserved keyword or has another editor";
}
}
if (!_.isEmpty(errors)) return errors;
}
});
if (!CMS.Views['Settings']) CMS.Views.Settings = {};
CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.Advanced
events : {
'click .delete-button' : "deleteEntry",
'click .save-button' : "saveView",
'click .cancel-button' : "revertView",
'click .new-button' : "addEntry",
// update model on changes
'change #course-advanced-policy-key' : "updateKey",
'change #course-advanced-policy-value' : "updateValue"
// TODO enable/disable save (add disabled class) based on validation & dirty
// TODO enable/disable new button?
},
initialize : function() {
var self = this;
// instantiates an editor template for each update in the collection
window.templateLoader.loadRemoteTemplate("advanced_entry",
"/static/client_templates/advanced_entry.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
this.model.on('error', this.handleValidationError, this);
},
render: function() {
// catch potential outside call before template loaded
if (!this.template) return this;
var listEle$ = this.$el.find('.course-advanced-policy-list');
listEle$.empty();
// same object so manipulations to one keep the other up to date
this.fieldToSelectorMap = this.selectorToField = {};
// iterate through model and produce key : value editors for each property in model.get
var self = this;
_.each(this.model.attributes,
function(value, key) {
listEle$.append(self.template({ key : key, value : value}));
self.fieldToSelectorMap[key] = key;
});
// insert the empty one
this.addEntry();
// Should this be on an event rather than render?
// var editor = ace.edit('course-advanced-policy-1-value');
// editor.setTheme("ace/theme/monokai");
// editor.getSession().setMode("ace/mode/javascript");
return this;
},
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.fieldToSelectorMap[key];
if (key !== '__new_advanced_key__') {
this.model.deleteKeys.push(key);
delete this.model[key];
}
li$.remove();
},
saveView : function(event) {
// TODO one last verification scan:
// call validateKey on each to ensure proper format
// check for dupes
this.model.save({
success : function() { window.alert("Saved"); },
error : CMS.ServerError
});
// FIXME don't delete if the validation didn't succeed in the save call
// remove deleted attrs
if (!_.isEmpty(this.model.deleteKeys)) {
var self = this;
// hmmm, not sure how to do this via backbone since we're not destroying the model
$.ajax({
url : this.model.url,
// json to and fro
contentType : "application/json",
dataType : "json",
// delete
type : 'DELETE',
// data
data : JSON.stringify({ deleteKeys : this.model.deleteKeys})
})
.fail(function(hdr, status, error) { CMS.ServerError(self.model, "Deleting keys:" + status); })
.done(function(data, status, error) {
// clear deleteKeys on success
self.model.deleteKeys = [];
});
}
},
revertView : function(event) {
this.model.deleteKeys = [];
var self = this;
this.model.fetch({
success : this.render,
error : CMS.ServerError
});
},
addEntry : function() {
var listEle$ = this.$el.find('.course-advanced-policy-list');
listEle$.append(this.template({ key : "", value : ""}));
// disable the value entry until there's an acceptable key
listEle$.find('.course-advanced-policy-value').addClass('disabled');
this.fieldToSelectorMap['__new_advanced_key__'] = '__new_advanced_key__';
},
updateKey : function(event) {
// old key: either the key as in the model or __new_advanced_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();
console.log('update ', oldKey, newKey); // REMOVE ME
if (oldKey !== newKey) {
// may erase other errors but difficult to just remove these
this.clearValidationErrors();
if (!this.validateKey(oldKey, newKey)) return;
if (this.model.has(newKey)) {
console.log('dupe key');
var error = {};
error[oldKey] = newKey + " has another entry";
error[newKey] = "Other entry for " + newKey;
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 === '__new_advanced_key__' ? '' : this.model.get(oldKey));
var validation = this.model.validate(newEntryModel);
if (validation) {
console.log('reserved key');
this.model.trigger("error", this.model, validation);
// abandon update
return;
}
// Now safe to actually do the update
this.model.set(newEntryModel);
delete this.fieldToSelectorMap[oldKey];
if (oldKey !== '__new_advanced_key__') {
// mark the old key for deletion and delete from field maps
this.model.deleteKeys.push(oldKey);
this.model.unset(oldKey) ;
}
else {
// enable the value entry
this.$el.find('.course-advanced-policy-value').removeClass('disabled');
}
// update gui (sets all the ids etc)
$(event.currentTarget).closest('li').replaceWith(this.template({key : newKey, value : this.model.get(newKey) }));
this.fieldToSelectorMap[newKey] = 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
if (_.isEmpty(newKey)) {
console.log('no key');
var error = {};
error[oldKey] = "Key cannot be an empty string";
this.model.trigger("error", this.model, error);
return false;
}
else return true;
},
updateValue : function(event) {
// much simpler than key munging. just update the value
var key = $(event.currentTarget).closest('.row').children('.key').attr('id');
var value = $(event.currentTarget).val();
console.log('updating ', key, value);
this.model.set(key, value, {validate:true});
}
});
......@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.12",
templateVersion: "0.0.13",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
......
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">settings</%block>
<%block name="title">Settings</%block>
......@@ -15,24 +16,28 @@ from contentstore import utils
<script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_details.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_settings.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script>
<script type="text/javascript">
$(document).ready(function(){
// 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});
advancedModel.blacklistKeys = ${advanced_blacklist | n};
advancedModel.url = "${reverse('course_advanced_settings', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var settingsModel = new CMS.Models.Settings.CourseSettings({
courseLocation: new CMS.Models.Location('${context_course.location}',{parse:true}),
details: new CMS.Models.Settings.CourseDetails(${course_details|n},{parse:true})
details: new CMS.Models.Settings.CourseDetails(${course_details|n},{parse:true}),
advanced: advancedModel
});
var advancedSettingsModel = new CMS.Models.Settings.Advanced({});
var editor = new CMS.Views.Settings.Main({
el: $('.main-wrapper'),
model : settingsModel
......@@ -743,90 +748,6 @@ from contentstore import utils
<!-- basic empty & initial empty field (if user had no values yet) -->
<ul class="input-list course-advanced-policy-list">
<li class="input multi course-advanced-policy-list-item">
<div class="row">
<div class="key">
<label for="course-advanced-policy-1-key">Policy Key:</label>
<div class="field">
<input type="text" class="short" id="course-advanced-policy-1-key" value="" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
</div>
</div>
<div class="value">
<label for="course-advanced-policy-1-value">Policy Value:</label>
<div class="field">
<div class="ace text" id="course-advanced-policy-1-value">some existing text</div>
</div>
</div>
</div>
<a href="#" class="delete-button standard remove-item advanced-policy-data"><span class="delete-icon"></span>Delete</a>
</li>
<!-- error existing key pair example -->
<li class="input multi course-advanced-policy-list-item">
<div class="row">
<div class="key">
<label for="course-advanced-policy-2-key">Policy Key:</label>
<div class="field">
<input type="text" class="short" id="course-advanced-policy-2-key" value="" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
</div>
</div>
<div class="value">
<label for="course-advanced-policy-2-value">Policy Value:</label>
<div class="field">
<textarea class="ace text" id="course-advanced-policy-2-value"></textarea>
</div>
</div>
</div>
<span class="message-error">This policy key, $KEYNAME, already exists.</span>
<a href="#" class="delete-button standard remove-item advanced-policy-data"><span class="delete-icon"></span>Delete</a>
</li>
<!-- error on key left empty example -->
<li class="input multi course-advanced-policy-list-item">
<div class="row">
<div class="key error">
<label for="course-advanced-policy-3-key">Policy Key:</label>
<div class="field">
<input type="text" class="short" id="course-advanced-policy-3-key" value="" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
</div>
</div>
<div class="value error">
<label for="course-advanced-policy-3-value">Policy Value:</label>
<div class="field">
<textarea class="ace text" id="course-advanced-policy-3-value"></textarea>
</div>
</div>
</div>
<span class="message-error">You cannot leave the key value for this pair blank.</span>
<a href="#" class="delete-button standard remove-item advanced-policy-data"><span class="delete-icon"></span>Delete</a>
</li>
<!-- error with value formatting example -->
<li class="input multi course-advanced-policy-list-item">
<div class="row">
<div class="key error">
<label for="course-advanced-policy-4-key">Policy Key:</label>
<div class="field">
<input type="text" class="short" id="course-advanced-policy-4-key" value="" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
</div>
</div>
<div class="value error">
<label for="course-advanced-policy-4-value">Policy Value:</label>
<div class="field">
<textarea class="ace text" id="course-advanced-policy-4-value" value=""></textarea>
</div>
</div>
</div>
<span class="message-error">The JSON value for $KEYNAME is invalid.</span>
<a href="#" class="delete-button standard remove-item advanced-policy-data"><span class="delete-icon"></span>Delete</a>
</li>
</ul>
<!-- advanced policy actions -->
......@@ -838,14 +759,6 @@ from contentstore import utils
</a>
</div>
<!-- advanced policy actions (with disabled save state) -->
<div class="actions actions-advanced-policies">
<a href="#" class="save-button disabled">Save</a>
<a href="#" class="cancel-button">Cancel</a>
<a href="#" class="new-button new-advanced-policy-item add-policy-data">
<span class="plus-icon white"></span>New Manual Policy
</a>
</div>
</div>
</div>
</section><!-- .settings-advanced-policies -->
......
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