diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index bd0dc2fb48..577492753b 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -1109,8 +1109,31 @@ def get_course_settings(request, org, course, name):
course_details = CourseDetails.fetch(location)
return render_to_response('settings.html', {
- 'active_tab': 'settings',
'context_course': course_module,
+ 'course_location' : location,
+ 'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
+ })
+
+@login_required
+@ensure_csrf_cookie
+def course_config_graders_page(request, org, course, name):
+ """
+ Send models and views as well as html for editing the course settings to the client.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = ['i4x', org, course, 'course', name]
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ course_module = modulestore().get_item(location)
+ course_details = CourseGradingModel.fetch(location)
+
+ return render_to_response('settings_graders.html', {
+ 'context_course': course_module,
+ 'course_location' : location,
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
})
diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js
index bdbb46b3b1..5090b5006d 100644
--- a/cms/static/js/models/settings/course_details.js
+++ b/cms/static/js/models/settings/course_details.js
@@ -1,85 +1,85 @@
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
- defaults: {
- location : null, // the course's Location model, required
- start_date: null, // maps to 'start'
- end_date: null, // maps to 'end'
- enrollment_start: null,
- enrollment_end: null,
- syllabus: null,
- overview: "",
- intro_video: null,
- effort: null // an int or null
- },
-
- // When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
- parse: function(attributes) {
- if (attributes['course_location']) {
- attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
- }
- if (attributes['start_date']) {
- attributes.start_date = new Date(attributes.start_date);
- }
- if (attributes['end_date']) {
- attributes.end_date = new Date(attributes.end_date);
- }
- if (attributes['enrollment_start']) {
- attributes.enrollment_start = new Date(attributes.enrollment_start);
- }
- if (attributes['enrollment_end']) {
- attributes.enrollment_end = new Date(attributes.enrollment_end);
- }
- return attributes;
- },
-
- validate: function(newattrs) {
- // Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
- // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
- var errors = {};
- if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
- errors.end_date = "The course end date cannot be before the course start date.";
- }
- if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
- errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
- }
- if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
- errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
- }
- if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
- errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
- }
- if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
- if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
- errors.intro_video = "Key should only contain letters, numbers, _, or -";
- }
- // TODO check if key points to a real video using google's youtube api
- }
- if (!_.isEmpty(errors)) return errors;
- // NOTE don't return empty errors as that will be interpreted as an error state
- },
-
- url: function() {
- var location = this.get('location');
- return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
- },
-
- _videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
- save_videosource: function(newsource) {
- // newsource either is or just the "speed:key, *" string
- // returns the videosource for the preview which iss the key whose speed is closest to 1
- if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null},
- { error : CMS.ServerError});
- // TODO remove all whitespace w/in string
- else {
- if (this.get('intro_video') !== newsource) this.save('intro_video', newsource,
- { error : CMS.ServerError});
- }
-
- return this.videosourceSample();
- },
- videosourceSample : function() {
- if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
- else return "";
- }
+ defaults: {
+ location : null, // the course's Location model, required
+ start_date: null, // maps to 'start'
+ end_date: null, // maps to 'end'
+ enrollment_start: null,
+ enrollment_end: null,
+ syllabus: null,
+ overview: "",
+ intro_video: null,
+ effort: null // an int or null
+ },
+
+ // When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
+ parse: function(attributes) {
+ if (attributes['course_location']) {
+ attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
+ }
+ if (attributes['start_date']) {
+ attributes.start_date = new Date(attributes.start_date);
+ }
+ if (attributes['end_date']) {
+ attributes.end_date = new Date(attributes.end_date);
+ }
+ if (attributes['enrollment_start']) {
+ attributes.enrollment_start = new Date(attributes.enrollment_start);
+ }
+ if (attributes['enrollment_end']) {
+ attributes.enrollment_end = new Date(attributes.enrollment_end);
+ }
+ return attributes;
+ },
+
+ validate: function(newattrs) {
+ // Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
+ // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
+ var errors = {};
+ if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
+ errors.end_date = "The course end date cannot be before the course start date.";
+ }
+ if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
+ errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
+ }
+ if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
+ errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
+ }
+ if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
+ errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
+ }
+ if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
+ if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
+ errors.intro_video = "Key should only contain letters, numbers, _, or -";
+ }
+ // TODO check if key points to a real video using google's youtube api
+ }
+ if (!_.isEmpty(errors)) return errors;
+ // NOTE don't return empty errors as that will be interpreted as an error state
+ },
+
+ url: function() {
+ var location = this.get('location');
+ return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
+ },
+
+ _videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
+ save_videosource: function(newsource) {
+ // newsource either is or just the "speed:key, *" string
+ // returns the videosource for the preview which iss the key whose speed is closest to 1
+ if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null},
+ { error : CMS.ServerError});
+ // TODO remove all whitespace w/in string
+ else {
+ if (this.get('intro_video') !== newsource) this.save('intro_video', newsource,
+ { error : CMS.ServerError});
+ }
+
+ return this.videosourceSample();
+ },
+ videosourceSample : function() {
+ if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
+ else return "";
+ }
});
diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js
index cce4e0207d..7722a27f67 100644
--- a/cms/static/js/models/settings/course_grading_policy.js
+++ b/cms/static/js/models/settings/course_grading_policy.js
@@ -1,55 +1,56 @@
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
- defaults : {
- course_location : null,
- graders : null, // CourseGraderCollection
- grade_cutoffs : null, // CourseGradeCutoff model
+ defaults : {
+ course_location : null,
+ graders : null, // CourseGraderCollection
+ grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or { hours: n, minutes: m, ...}
- },
- parse: function(attributes) {
- if (attributes['course_location']) {
- attributes.course_location = new CMS.Models.Location(attributes.course_location, {parse:true});
- }
- if (attributes['graders']) {
- var graderCollection;
- if (this.has('graders')) {
- graderCollection = this.get('graders');
- graderCollection.reset(attributes.graders);
- }
- else {
- graderCollection = new CMS.Models.Settings.CourseGraderCollection(attributes.graders);
- graderCollection.course_location = attributes['course_location'] || this.get('course_location');
- }
- attributes.graders = graderCollection;
- }
- return attributes;
- },
- url : function() {
- var location = this.get('course_location');
- return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/grading';
- },
- gracePeriodToDate : function() {
- var newDate = new Date();
- if (this.has('grace_period') && this.get('grace_period')['hours'])
- newDate.setHours(this.get('grace_period')['hours']);
- else newDate.setHours(0);
- if (this.has('grace_period') && this.get('grace_period')['minutes'])
- newDate.setMinutes(this.get('grace_period')['minutes']);
- else newDate.setMinutes(0);
- if (this.has('grace_period') && this.get('grace_period')['seconds'])
- newDate.setSeconds(this.get('grace_period')['seconds']);
- else newDate.setSeconds(0);
-
- return newDate;
- },
- dateToGracePeriod : function(date) {
- return {hours : date.getHours(), minutes : date.getMinutes(), seconds : date.getSeconds() };
- }
+ },
+ parse: function(attributes) {
+ if (attributes['course_location']) {
+ attributes.course_location = new CMS.Models.Location(attributes.course_location, {parse:true});
+ }
+ if (attributes['graders']) {
+ var graderCollection;
+ // interesting race condition: if {parse:true} when newing, then parse called before .attributes created
+ if (this.attributes && this.has('graders')) {
+ graderCollection = this.get('graders');
+ graderCollection.reset(attributes.graders);
+ }
+ else {
+ graderCollection = new CMS.Models.Settings.CourseGraderCollection(attributes.graders);
+ graderCollection.course_location = attributes['course_location'] || this.get('course_location');
+ }
+ attributes.graders = graderCollection;
+ }
+ return attributes;
+ },
+ url : function() {
+ var location = this.get('course_location');
+ return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/grading';
+ },
+ gracePeriodToDate : function() {
+ var newDate = new Date();
+ if (this.has('grace_period') && this.get('grace_period')['hours'])
+ newDate.setHours(this.get('grace_period')['hours']);
+ else newDate.setHours(0);
+ if (this.has('grace_period') && this.get('grace_period')['minutes'])
+ newDate.setMinutes(this.get('grace_period')['minutes']);
+ else newDate.setMinutes(0);
+ if (this.has('grace_period') && this.get('grace_period')['seconds'])
+ newDate.setSeconds(this.get('grace_period')['seconds']);
+ else newDate.setSeconds(0);
+
+ return newDate;
+ },
+ dateToGracePeriod : function(date) {
+ return {hours : date.getHours(), minutes : date.getMinutes(), seconds : date.getSeconds() };
+ }
});
CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
- defaults: {
+ defaults: {
"type" : "", // must be unique w/in collection (ie. w/in course)
"min_count" : 1,
"drop_count" : 0,
@@ -57,71 +58,71 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
"weight" : 0 // int 0..100
},
parse : function(attrs) {
- if (attrs['weight']) {
- if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight);
- }
- if (attrs['min_count']) {
- if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count);
- }
- if (attrs['drop_count']) {
- if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count);
- }
- return attrs;
+ if (attrs['weight']) {
+ if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight);
+ }
+ if (attrs['min_count']) {
+ if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count);
+ }
+ if (attrs['drop_count']) {
+ if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count);
+ }
+ return attrs;
},
validate : function(attrs) {
- var errors = {};
- if (attrs['type']) {
- if (_.isEmpty(attrs['type'])) {
- errors.type = "The assignment type must have a name.";
- }
- else {
- // FIXME somehow this.collection is unbound sometimes. I can't track down when
- var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this);
- if (existing) {
- errors.type = "There's already another assignment type with this name.";
- }
- }
- }
- if (attrs['weight']) {
- if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) {
- errors.weight = "Please enter an integer between 0 and 100.";
- }
- else {
- attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int
- if (this.collection && attrs.weight > 0) {
- // FIXME b/c saves don't update the models if validation fails, we should
- // either revert the field value to the one in the model and make them make room
- // or figure out a wholistic way to balance the vals across the whole
-// if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
-// errors.weight = "The weights cannot add to more than 100.";
- }
- }}
- if (attrs['min_count']) {
- if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
- errors.min_count = "Please enter an integer.";
- }
- else attrs.min_count = parseInt(attrs.min_count);
- }
- if (attrs['drop_count']) {
- if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
- errors.drop_count = "Please enter an integer.";
- }
- else attrs.drop_count = parseInt(attrs.drop_count);
- }
- if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) {
- errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned.";
- }
- if (!_.isEmpty(errors)) return errors;
+ var errors = {};
+ if (attrs['type']) {
+ if (_.isEmpty(attrs['type'])) {
+ errors.type = "The assignment type must have a name.";
+ }
+ else {
+ // FIXME somehow this.collection is unbound sometimes. I can't track down when
+ var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this);
+ if (existing) {
+ errors.type = "There's already another assignment type with this name.";
+ }
+ }
+ }
+ if (attrs['weight']) {
+ if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) {
+ errors.weight = "Please enter an integer between 0 and 100.";
+ }
+ else {
+ attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int
+ if (this.collection && attrs.weight > 0) {
+ // FIXME b/c saves don't update the models if validation fails, we should
+ // either revert the field value to the one in the model and make them make room
+ // or figure out a wholistic way to balance the vals across the whole
+// if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
+// errors.weight = "The weights cannot add to more than 100.";
+ }
+ }}
+ if (attrs['min_count']) {
+ if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
+ errors.min_count = "Please enter an integer.";
+ }
+ else attrs.min_count = parseInt(attrs.min_count);
+ }
+ if (attrs['drop_count']) {
+ if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
+ errors.drop_count = "Please enter an integer.";
+ }
+ else attrs.drop_count = parseInt(attrs.drop_count);
+ }
+ if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) {
+ errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned.";
+ }
+ if (!_.isEmpty(errors)) return errors;
}
});
CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
- model : CMS.Models.Settings.CourseGrader,
- course_location : null, // must be set to a Location object
- url : function() {
- return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/grades/' + this.course_location.get('name') + '/';
- },
- sumWeights : function() {
- return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
- }
+ model : CMS.Models.Settings.CourseGrader,
+ course_location : null, // must be set to a Location object
+ url : function() {
+ return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/grades/' + this.course_location.get('name') + '/';
+ },
+ sumWeights : function() {
+ return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
+ }
});
\ No newline at end of file
diff --git a/cms/static/js/models/settings/course_settings.js b/cms/static/js/models/settings/course_settings.js
index 9d09e4bdc5..62b214e853 100644
--- a/cms/static/js/models/settings/course_settings.js
+++ b/cms/static/js/models/settings/course_settings.js
@@ -1,43 +1,42 @@
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseSettings = Backbone.Model.extend({
- // a container for the models representing the n possible tabbed states
- defaults: {
- courseLocation: null,
- // NOTE: keep these sync'd w/ the data-section names in settings-page-menu
- details: null,
- faculty: null,
- grading: null,
- problems: null,
- discussions: null
- },
+ // a container for the models representing the n possible tabbed states
+ defaults: {
+ courseLocation: null,
+ details: null,
+ faculty: null,
+ grading: null,
+ problems: null,
+ discussions: null
+ },
- retrieve: function(submodel, callback) {
- if (this.get(submodel)) callback();
- else {
- var cachethis = this;
- switch (submodel) {
- case 'details':
- var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
- details.fetch( {
- success : function(model) {
- cachethis.set('details', model);
- callback(model);
- }
- });
- break;
- case 'grading':
- var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
- grading.fetch( {
- success : function(model) {
- cachethis.set('grading', model);
- callback(model);
- }
- });
- break;
+ retrieve: function(submodel, callback) {
+ if (this.get(submodel)) callback();
+ else {
+ var cachethis = this;
+ switch (submodel) {
+ case 'details':
+ var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
+ details.fetch( {
+ success : function(model) {
+ cachethis.set('details', model);
+ callback(model);
+ }
+ });
+ break;
+ case 'grading':
+ var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
+ grading.fetch( {
+ success : function(model) {
+ cachethis.set('grading', model);
+ callback(model);
+ }
+ });
+ break;
- default:
- break;
- }
- }
- }
+ default:
+ break;
+ }
+ }
+ }
})
\ No newline at end of file
diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js
index 826b385dff..4523bb6ade 100644
--- a/cms/static/js/views/settings/main_settings_view.js
+++ b/cms/static/js/views/settings/main_settings_view.js
@@ -1,216 +1,85 @@
-if (!CMS.Views['Settings']) CMS.Views.Settings = {};
-
-// TODO move to common place
-CMS.Views.ValidatingView = Backbone.View.extend({
- // Intended as an abstract class which catches validation errors on the model and
- // decorates the fields. Needs wiring per class, but this initialization shows how
- // either have your init call this one or copy the contents
- initialize : function() {
- this.model.on('error', this.handleValidationError, this);
- this.selectorToField = _.invert(this.fieldToSelectorMap);
- },
-
- errorTemplate : _.template('<%= message %>'),
-
- events : {
- "blur input" : "clearValidationErrors",
- "blur textarea" : "clearValidationErrors"
- },
- fieldToSelectorMap : {
- // Your subclass must populate this w/ all of the model keys and dom selectors
- // which may be the subjects of validation errors
- },
- _cacheValidationErrors : [],
- handleValidationError : function(model, error) {
- // error is object w/ fields and error strings
- for (var field in error) {
- var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
- this._cacheValidationErrors.push(ele);
- if ($(ele).is('div')) {
- // put error on the contained inputs
- $(ele).find('input, textarea').addClass('error');
- }
- else $(ele).addClass('error');
- $(ele).parent().append(this.errorTemplate({message : error[field]}));
- }
- },
-
- clearValidationErrors : function() {
- // error is object w/ fields and error strings
- while (this._cacheValidationErrors.length > 0) {
- var ele = this._cacheValidationErrors.pop();
- if ($(ele).is('div')) {
- // put error on the contained inputs
- $(ele).find('input, textarea').removeClass('error');
- }
- else $(ele).removeClass('error');
- $(ele).nextAll('.message-error').remove();
- }
- },
-
- saveIfChanged : function(event) {
- // returns true if the value changed and was thus sent to server
- var field = this.selectorToField[event.currentTarget.id];
- var currentVal = this.model.get(field);
- var newVal = $(event.currentTarget).val();
- if (currentVal != newVal) {
- this.clearValidationErrors();
- this.model.save(field, newVal, { error : CMS.ServerError});
- return true;
- }
- else return false;
- }
-});
-
-CMS.Views.Settings.Main = Backbone.View.extend({
- // Model class is CMS.Models.Settings.CourseSettings
- // allow navigation between the tabs
- events: {
- 'click .settings-page-menu a': "showSettingsTab",
- 'mouseover #timezone' : "updateTime"
- },
-
- currentTab: null,
- subviews: {}, // indexed by tab name
-
- initialize: function() {
- // load templates
- this.currentTab = this.$el.find('.settings-page-menu .is-shown').attr('data-section');
- // create the initial subview
- this.subviews[this.currentTab] = this.createSubview();
-
- // fill in fields
- this.$el.find("#course-name").val(this.model.get('courseLocation').get('name'));
- this.$el.find("#course-organization").val(this.model.get('courseLocation').get('org'));
- this.$el.find("#course-number").val(this.model.get('courseLocation').get('course'));
- this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
- this.$el.find(":input, textarea").focus(function() {
- $("label[for='" + this.id + "']").addClass("is-focused");
- }).blur(function() {
- $("label").removeClass("is-focused");
- });
- this.render();
- },
-
- render: function() {
-
- // create any necessary subviews and put them onto the page
- if (!this.model.has(this.currentTab)) {
- // TODO disable screen until fetch completes?
- var cachethis = this;
- this.model.retrieve(this.currentTab, function() {
- cachethis.subviews[cachethis.currentTab] = cachethis.createSubview();
- cachethis.subviews[cachethis.currentTab].render();
- });
- }
- else this.subviews[this.currentTab].render();
-
- var dateIntrospect = new Date();
- this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
-
- return this;
- },
-
- createSubview: function() {
- switch (this.currentTab) {
- case 'details':
- return new CMS.Views.Settings.Details({
- el: this.$el.find('.settings-' + this.currentTab),
- model: this.model.get(this.currentTab)
- });
- case 'faculty':
- break;
- case 'grading':
- return new CMS.Views.Settings.Grading({
- el: this.$el.find('.settings-' + this.currentTab),
- model: this.model.get(this.currentTab)
- });
- case 'problems':
- break;
- case 'discussions':
- break;
- }
- },
-
- updateTime : function(e) {
- var now = new Date();
- var hours = now.getHours();
- var minutes = now.getMinutes();
- $(e.currentTarget).attr('title', (hours % 12 === 0 ? 12 : hours % 12) + ":" + (minutes < 10 ? "0" : "") +
- now.getMinutes() + (hours < 12 ? "am" : "pm") + " (current local time)");
- },
-
- showSettingsTab: function(e) {
- this.currentTab = $(e.target).attr('data-section');
- $('.settings-page-section > section').hide();
- $('.settings-' + this.currentTab).show();
- $('.settings-page-menu .is-shown').removeClass('is-shown');
- $(e.target).addClass('is-shown');
- // fetch model for the tab if not loaded already
- this.render();
- }
-
-});
+if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg exists
CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
- // Model class is CMS.Models.Settings.CourseDetails
- events : {
- "blur input" : "updateModel",
- "blur textarea" : "updateModel",
- 'click .remove-course-syllabus' : "removeSyllabus",
- 'click .new-course-syllabus' : 'assetSyllabus',
- 'click .remove-course-introduction-video' : "removeVideo",
- 'focus #course-overview' : "codeMirrorize"
- },
- initialize : function() {
- // TODO move the html frag to a loaded asset
- this.fileAnchorTemplate = _.template(' 📄<%= filename %>');
- this.model.on('error', this.handleValidationError, this);
- this.selectorToField = _.invert(this.fieldToSelectorMap);
- },
-
- render: function() {
- this.setupDatePicker('start_date');
- this.setupDatePicker('end_date');
- this.setupDatePicker('enrollment_start');
- this.setupDatePicker('enrollment_end');
-
- if (this.model.has('syllabus')) {
- this.$el.find(this.fieldToSelectorMap['syllabus']).html(
- this.fileAnchorTemplate({
- fullpath : this.model.get('syllabus'),
- filename: 'syllabus'}));
- this.$el.find('.remove-course-syllabus').show();
- }
- else {
- this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html("");
- this.$el.find('.remove-course-syllabus').hide();
- }
-
- this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
- this.codeMirrorize(null, $('#course-overview')[0]);
-
- this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
- if (this.model.has('intro_video')) {
- this.$el.find('.remove-course-introduction-video').show();
- this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
- }
- else this.$el.find('.remove-course-introduction-video').hide();
-
- this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
-
- return this;
- },
- fieldToSelectorMap : {
- 'start_date' : "course-start",
- 'end_date' : 'course-end',
- 'enrollment_start' : 'enrollment-start',
- 'enrollment_end' : 'enrollment-end',
- 'syllabus' : '.current-course-syllabus .doc-filename',
- 'overview' : 'course-overview',
- 'intro_video' : 'course-introduction-video',
- 'effort' : "course-effort"
- },
+ // Model class is CMS.Models.Settings.CourseDetails
+ events : {
+ "blur input" : "updateModel",
+ "blur textarea" : "updateModel",
+ 'click .remove-course-syllabus' : "removeSyllabus",
+ 'click .new-course-syllabus' : 'assetSyllabus',
+ 'click .remove-course-introduction-video' : "removeVideo",
+ 'focus #course-overview' : "codeMirrorize",
+ 'mouseover #timezone' : "updateTime",
+ // would love to move to a general superclass, but event hashes don't inherit in backbone :-(
+ 'focus :input' : "inputFocus",
+ 'blur :input' : "inputUnfocus"
+
+ },
+ initialize : function() {
+ this.fileAnchorTemplate = _.template(' 📄<%= filename %>');
+ // fill in fields
+ this.$el.find("#course-name").val(this.model.get('location').get('name'));
+ this.$el.find("#course-organization").val(this.model.get('location').get('org'));
+ this.$el.find("#course-number").val(this.model.get('location').get('course'));
+ this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
+
+ var dateIntrospect = new Date();
+ this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
+
+ this.model.on('error', this.handleValidationError, this);
+ this.selectorToField = _.invert(this.fieldToSelectorMap);
+ },
+
+ render: function() {
+ this.setupDatePicker('start_date');
+ this.setupDatePicker('end_date');
+ this.setupDatePicker('enrollment_start');
+ this.setupDatePicker('enrollment_end');
+
+ if (this.model.has('syllabus')) {
+ this.$el.find(this.fieldToSelectorMap['syllabus']).html(
+ this.fileAnchorTemplate({
+ fullpath : this.model.get('syllabus'),
+ filename: 'syllabus'}));
+ this.$el.find('.remove-course-syllabus').show();
+ }
+ else {
+ this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html("");
+ this.$el.find('.remove-course-syllabus').hide();
+ }
+
+ this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
+ this.codeMirrorize(null, $('#course-overview')[0]);
+
+ this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
+ if (this.model.has('intro_video')) {
+ this.$el.find('.remove-course-introduction-video').show();
+ this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
+ }
+ else this.$el.find('.remove-course-introduction-video').hide();
+
+ this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
+
+ return this;
+ },
+ fieldToSelectorMap : {
+ 'start_date' : "course-start",
+ 'end_date' : 'course-end',
+ 'enrollment_start' : 'enrollment-start',
+ 'enrollment_end' : 'enrollment-end',
+ 'syllabus' : '.current-course-syllabus .doc-filename',
+ 'overview' : 'course-overview',
+ 'intro_video' : 'course-introduction-video',
+ 'effort' : "course-effort"
+ },
+
+ updateTime : function(e) {
+ var now = new Date();
+ var hours = now.getHours();
+ var minutes = now.getMinutes();
+ $(e.currentTarget).attr('title', (hours % 12 === 0 ? 12 : hours % 12) + ":" + (minutes < 10 ? "0" : "") +
+ now.getMinutes() + (hours < 12 ? "am" : "pm") + " (current local time)");
+ },
setupDatePicker: function (fieldName) {
var cacheModel = this.model;
@@ -245,58 +114,58 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
datefield.datepicker('setDate', this.model.get(fieldName));
if (this.model.has(fieldName)) timefield.timepicker('setTime', this.model.get(fieldName));
},
-
- updateModel: function(event) {
- switch (event.currentTarget.id) {
- case 'course-start-date': // handled via onSelect method
- case 'course-end-date':
- case 'course-enrollment-start-date':
- case 'course-enrollment-end-date':
- break;
- case 'course-overview':
- // handled via code mirror
- break;
+ updateModel: function(event) {
+ switch (event.currentTarget.id) {
+ case 'course-start-date': // handled via onSelect method
+ case 'course-end-date':
+ case 'course-enrollment-start-date':
+ case 'course-enrollment-end-date':
+ break;
- case 'course-effort':
- this.saveIfChanged(event);
- break;
- case 'course-introduction-video':
- this.clearValidationErrors();
- var previewsource = this.model.save_videosource($(event.currentTarget).val());
- this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
- if (this.model.has('intro_video')) {
- this.$el.find('.remove-course-introduction-video').show();
- }
- else {
- this.$el.find('.remove-course-introduction-video').hide();
- }
- break;
-
- default:
- break;
- }
-
- },
-
- removeSyllabus: function() {
- if (this.model.has('syllabus')) this.model.save({'syllabus': null},
- { error : CMS.ServerError});
- },
-
- assetSyllabus : function() {
- // TODO implement
- },
-
- removeVideo: function() {
- if (this.model.has('intro_video')) {
- this.model.save_videosource(null);
- this.$el.find(".current-course-introduction-video iframe").attr("src", "");
- this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val("");
- this.$el.find('.remove-course-introduction-video').hide();
- }
- },
- codeMirrors : {},
+ case 'course-overview':
+ // handled via code mirror
+ break;
+
+ case 'course-effort':
+ this.saveIfChanged(event);
+ break;
+ case 'course-introduction-video':
+ this.clearValidationErrors();
+ var previewsource = this.model.save_videosource($(event.currentTarget).val());
+ this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource);
+ if (this.model.has('intro_video')) {
+ this.$el.find('.remove-course-introduction-video').show();
+ }
+ else {
+ this.$el.find('.remove-course-introduction-video').hide();
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ },
+
+ removeSyllabus: function() {
+ if (this.model.has('syllabus')) this.model.save({'syllabus': null},
+ { error : CMS.ServerError});
+ },
+
+ assetSyllabus : function() {
+ // TODO implement
+ },
+
+ removeVideo: function() {
+ if (this.model.has('intro_video')) {
+ this.model.save_videosource(null);
+ this.$el.find(".current-course-introduction-video iframe").attr("src", "");
+ this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val("");
+ this.$el.find('.remove-course-introduction-video').hide();
+ }
+ },
+ codeMirrors : {},
codeMirrorize: function (e, forcedTarget) {
var thisTarget;
if (forcedTarget) {
@@ -316,373 +185,10 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
cachethis.clearValidationErrors();
var newVal = mirror.getValue();
if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal,
- { error: CMS.ServerError});
+ { error: CMS.ServerError});
}
});
}
}
-
-});
-
-CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
- // Model class is CMS.Models.Settings.CourseGradingPolicy
- events : {
- "blur input" : "updateModel",
- "blur textarea" : "updateModel",
- "blur span[contenteditable=true]" : "updateDesignation",
- "click .settings-extra header" : "showSettingsExtras",
- "click .new-grade-button" : "addNewGrade",
- "click .remove-button" : "removeGrade",
- "click .add-grading-data" : "addAssignmentType"
- },
- initialize : function() {
- // load template for grading view
- var self = this;
- this.gradeCutoffTemplate = _.template('
' +
- '<%= descriptor %>' +
- '' +
- '<% if (removable) {%>remove<% ;} %>' +
- '');
-
- // Instrument grading scale
- // convert cutoffs to inversely ordered list
- var modelCutoffs = this.model.get('grade_cutoffs');
- for (var cutoff in modelCutoffs) {
- this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
- }
- this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
- function (gradeEle) { return -gradeEle['cutoff']; });
-
- // Instrument grace period
- this.$el.find('#course-grading-graceperiod').timepicker();
-
- // instantiates an editor template for each update in the collection
- // Because this calls render, put it after everything which render may depend upon to prevent race condition.
- window.templateLoader.loadRemoteTemplate("course_grade_policy",
- "/static/client_templates/course_grade_policy.html",
- function (raw_template) {
- self.template = _.template(raw_template);
- self.render();
- }
- );
- this.model.on('error', this.handleValidationError, this);
- this.model.get('graders').on('remove', this.render, this);
- this.model.get('graders').on('reset', this.render, this);
- this.model.get('graders').on('add', this.render, this);
- this.selectorToField = _.invert(this.fieldToSelectorMap);
- },
-
- render: function() {
- // prevent bootstrap race condition by event dispatch
- if (!this.template) return;
-
- // Create and render the grading type subs
- var self = this;
- var gradelist = this.$el.find('.course-grading-assignment-list');
- // Undo the double invocation error. At some point, fix the double invocation
- $(gradelist).empty();
- var gradeCollection = this.model.get('graders');
- gradeCollection.each(function(gradeModel) {
- $(gradelist).append(self.template({model : gradeModel }));
- var newEle = gradelist.children().last();
- var newView = new CMS.Views.Settings.GraderView({el: newEle,
- model : gradeModel, collection : gradeCollection });
- });
-
- // render the grade cutoffs
- this.renderCutoffBar();
-
- var graceEle = this.$el.find('#course-grading-graceperiod');
- graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime
- if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate());
- // remove any existing listeners to keep them from piling on b/c render gets called frequently
- graceEle.off('change', this.setGracePeriod);
- graceEle.on('change', this, this.setGracePeriod);
-
- return this;
- },
- addAssignmentType : function(e) {
- e.preventDefault();
- this.model.get('graders').push({});
- },
- fieldToSelectorMap : {
- 'grace_period' : 'course-grading-graceperiod'
- },
- setGracePeriod : function(event) {
- event.data.clearValidationErrors();
- var newVal = event.data.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
- if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal,
- { error : CMS.ServerError});
- },
- updateModel : function(event) {
- if (!this.selectorToField[event.currentTarget.id]) return;
-
- switch (this.selectorToField[event.currentTarget.id]) {
- case 'grace_period': // handled above
- break;
-
- default:
- this.saveIfChanged(event);
- break;
- }
- },
-
- // Grade sliders attributes and methods
- // Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
- // The actual cutoff for each grade is the width % of the next lower grade; so, the hack here
- // is to lay down a whole width bar claiming it's A and then lay down bars for each actual grade
- // starting w/ A but posting the label in the preceding li and setting the label of the last to "Fail" or "F"
-
- // A does not have a drag bar (cannot change its upper limit)
- // Need to insert new bars in right place.
- GRADES : ['A', 'B', 'C', 'D'], // defaults for new grade designators
- descendingCutoffs : [], // array of { designation : , cutoff : }
- gradeBarWidth : null, // cache of value since it won't change (more certain)
-
- renderCutoffBar: function() {
- var gradeBar =this.$el.find('.grade-bar');
- this.gradeBarWidth = gradeBar.width();
- var gradelist = gradeBar.children('.grades');
- // HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
- gradelist.empty();
- var nextWidth = 100; // first width is 100%
- // Can probably be simplified to one variable now.
- var removable = false;
- var draggable = false; // first and last are not removable, first is not draggable
- _.each(this.descendingCutoffs,
- function(cutoff, index) {
- var newBar = this.gradeCutoffTemplate({
- descriptor : cutoff['designation'] ,
- width : nextWidth,
- removable : removable });
- gradelist.append(newBar);
- if (draggable) {
- newBar = gradelist.children().last(); // get the dom object not the unparsed string
- newBar.resizable({
- handles: "e",
- containment : "parent",
- start : this.startMoveClosure(),
- resize : this.moveBarClosure(),
- stop : this.stopDragClosure()
- });
- }
- // prepare for next
- nextWidth = cutoff['cutoff'];
- removable = true; // first is not removable, all others are
- draggable = true;
- },
- this);
- // add fail which is not in data
- var failBar = this.gradeCutoffTemplate({ descriptor : this.failLabel(),
- width : nextWidth, removable : false});
- $(failBar).find("span[contenteditable=true]").attr("contenteditable", false);
- gradelist.append(failBar);
- gradelist.children().last().resizable({
- handles: "e",
- containment : "parent",
- start : this.startMoveClosure(),
- resize : this.moveBarClosure(),
- stop : this.stopDragClosure()
- });
-
- this.renderGradeRanges();
- },
-
- showSettingsExtras : function(event) {
- $(event.currentTarget).toggleClass('active');
- $(event.currentTarget).siblings.toggleClass('is-shown');
- },
-
-
- startMoveClosure : function() {
- // set min/max widths
- var cachethis = this;
- var widthPerPoint = cachethis.gradeBarWidth / 100;
- return function(event, ui) {
- var barIndex = ui.element.index();
- // min and max represent limits not labels (note, can's make smaller than 3 points wide)
- var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
- // minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
- var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 97);
- ui.element.resizable("option",{minWidth : min * widthPerPoint, maxWidth : max * widthPerPoint});
- };
- },
-
- moveBarClosure : function() {
- // 0th ele doesn't have a bar; so, will never invoke this
- var cachethis = this;
- return function(event, ui) {
- var barIndex = ui.element.index();
- // min and max represent limits not labels (note, can's make smaller than 3 points wide)
- var min = (barIndex < cachethis.descendingCutoffs.length ? cachethis.descendingCutoffs[barIndex]['cutoff'] + 3 : 3);
- // minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
- var max = (barIndex >= 2 ? cachethis.descendingCutoffs[barIndex - 2]['cutoff'] - 3 : 100);
- var percentage = Math.min(Math.max(ui.size.width / cachethis.gradeBarWidth * 100, min), max);
- cachethis.descendingCutoffs[barIndex - 1]['cutoff'] = Math.round(percentage);
- cachethis.renderGradeRanges();
- };
- },
-
- renderGradeRanges: function() {
- // the labels showing the range e.g., 71-80
- var cutoffs = this.descendingCutoffs;
- this.$el.find('.range').each(function(i) {
- var min = (i < cutoffs.length ? cutoffs[i]['cutoff'] : 0);
- var max = (i > 0 ? cutoffs[i - 1]['cutoff'] : 100);
- $(this).text(min + '-' + max);
- });
- },
-
- stopDragClosure: function() {
- var cachethis = this;
- return function(event, ui) {
- // for some reason the resize is setting height to 0
- cachethis.saveCutoffs();
- };
- },
-
- saveCutoffs: function() {
- this.model.save('grade_cutoffs',
- _.reduce(this.descendingCutoffs,
- function(object, cutoff) {
- object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
- return object;
- },
- {}),
- { error : CMS.ServerError});
- },
-
- addNewGrade: function(e) {
- e.preventDefault();
- var gradeLength = this.descendingCutoffs.length; // cutoffs doesn't include fail/f so this is only the passing grades
- if(gradeLength > 3) {
- // TODO shouldn't we disable the button
- return;
- }
- var failBarWidth = this.descendingCutoffs[gradeLength - 1]['cutoff'];
- // going to split the grade above the insertion point in half leaving fail in same place
- var nextGradeTop = (gradeLength > 1 ? this.descendingCutoffs[gradeLength - 2]['cutoff'] : 100);
- var targetWidth = failBarWidth + ((nextGradeTop - failBarWidth) / 2);
- this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth});
- this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth);
-
- var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength],
- width : targetWidth, removable : true });
- var gradeDom = this.$el.find('.grades');
- gradeDom.children().last().before($newGradeBar);
- var newEle = gradeDom.children()[gradeLength];
- $(newEle).resizable({
- handles: "e",
- containment : "parent",
- start : this.startMoveClosure(),
- resize : this.moveBarClosure(),
- stop : this.stopDragClosure()
- });
-
- // Munge existing grade labels?
- // If going from Pass/Fail to 3 levels, change to Pass to A
- if (gradeLength === 1 && this.descendingCutoffs[0]['designation'] === 'Pass') {
- this.descendingCutoffs[0]['designation'] = this.GRADES[0];
- this.setTopGradeLabel();
- }
- this.setFailLabel();
-
- this.renderGradeRanges();
- this.saveCutoffs();
- },
-
- removeGrade: function(e) {
- e.preventDefault();
- var domElement = $(e.currentTarget).closest('li');
- var index = domElement.index();
- // copy the boundary up to the next higher grade then remove
- this.descendingCutoffs[index - 1]['cutoff'] = this.descendingCutoffs[index]['cutoff'];
- this.descendingCutoffs.splice(index, 1);
- domElement.remove();
-
- if (this.descendingCutoffs.length === 1 && this.descendingCutoffs[0]['designation'] === this.GRADES[0]) {
- this.descendingCutoffs[0]['designation'] = 'Pass';
- this.setTopGradeLabel();
- }
- this.setFailLabel();
- this.renderGradeRanges();
- this.saveCutoffs();
- },
-
- updateDesignation: function(e) {
- var index = $(e.currentTarget).closest('li').index();
- this.descendingCutoffs[index]['designation'] = $(e.currentTarget).html();
- this.saveCutoffs();
- },
-
- failLabel: function() {
- if (this.descendingCutoffs.length === 1) return 'Fail';
- else return 'F';
- },
- setFailLabel: function() {
- this.$el.find('.grades .letter-grade').last().html(this.failLabel());
- },
- setTopGradeLabel: function() {
- this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
- }
});
-
-CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
- // Model class is CMS.Models.Settings.CourseGrader
- events : {
- "blur input" : "updateModel",
- "blur textarea" : "updateModel",
- "click .remove-grading-data" : "deleteModel"
- },
- initialize : function() {
- this.model.on('error', this.handleValidationError, this);
- this.selectorToField = _.invert(this.fieldToSelectorMap);
- this.render();
- },
-
- render: function() {
- return this;
- },
- fieldToSelectorMap : {
- 'type' : 'course-grading-assignment-name',
- 'short_label' : 'course-grading-assignment-shortname',
- 'min_count' : 'course-grading-assignment-totalassignments',
- 'drop_count' : 'course-grading-assignment-droppable',
- 'weight' : 'course-grading-assignment-gradeweight'
- },
- updateModel : function(event) {
- // HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
- // this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
- // give 2 assignments the same name.]
- if (!this.model.collection) {
- this.model.collection = this.collection;
- }
-
- switch (event.currentTarget.id) {
- case 'course-grading-assignment-totalassignments':
- this.$el.find('#course-grading-assignment-droppable').attr('max', $(event.currentTarget).val());
- this.saveIfChanged(event);
- break;
- case 'course-grading-assignment-name':
- var oldName = this.model.get('type');
- if (this.saveIfChanged(event) && !_.isEmpty(oldName)) {
- // overload the error display logic
- this._cacheValidationErrors.push(event.currentTarget);
- $(event.currentTarget).parent().append(
- this.errorTemplate({message : 'For grading to work, you must change all "' + oldName +
- '" subsections to "' + this.model.get('type') + '".'}));
- }
- break;
- default:
- this.saveIfChanged(event);
- break;
- }
- },
- deleteModel : function(e) {
- this.model.destroy(
- { error : CMS.ServerError});
- e.preventDefault();
- }
-
-});
\ No newline at end of file
diff --git a/cms/templates/settings.html b/cms/templates/settings.html
index 9c204b750e..14d5283df0 100644
--- a/cms/templates/settings.html
+++ b/cms/templates/settings.html
@@ -1,5 +1,5 @@
<%inherit file="base.html" />
-<%block name="title">Settings%block>
+<%block name="title">Schedule & Details%block>
<%block name="bodyclass">is-signedin course settings%block>
@@ -16,24 +16,18 @@ from contentstore import utils
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+%block>
+
+<%block name="content">
+
+
+
+
Settings
+
+
+
+
+ Faculty
+
+
+
+ Faculty Members
+ Individuals instructing and help with this course
+
+
+
+
+
+
+
+
+ Problems
+
+
+
+ General Settings
+ Course-wide settings for all problems
+
+
+
+
Problem Randomization:
+
+
+
+
+
+
+
+
+
+
+
+
+ Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"
+
+
+
+
+
+
+
+
+
+ Discussions
+
+
+
+ General Settings
+ Course-wide settings for online discussion
+
+
+
+
Anonymous Discussions:
+
+
+
+
+
+
Anonymous Discussions:
+
+
+
+
+
+
Discussion Categories
+
+
+
+
+
+
+
+
+
+
+%block>
diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html
new file mode 100644
index 0000000000..654f9043e4
--- /dev/null
+++ b/cms/templates/settings_graders.html
@@ -0,0 +1,123 @@
+<%inherit file="base.html" />
+<%block name="title">Grading%block>
+<%block name="bodyclass">is-signedin course settings%block>
+
+
+<%namespace name='static' file='static_content.html'/>
+<%!
+from contentstore import utils
+%>
+
+
+<%block name="jsextra">
+
+
+
+
+
+
+
+
+
+
+
+%block>
+
+<%block name="content">
+
+
+
+
Settings
+
+
+
+
+ Grading
+
+
+
+ Overall Grade Range
+ Course grade ranges and their values
+
+
+
+
+
+
+
+
+
+ - 0
+ - 10
+ - 20
+ - 30
+ - 40
+ - 50
+ - 60
+ - 70
+ - 80
+ - 90
+ - 100
+
+
+
+
+
+
+
+
+
+
+
+
+ General Grading
+ Deadlines and Requirements
+
+
+
+
+
+
+
+
+ leeway on due dates
+
+
+
+
+
+
+
+
+
+
+
+
+
+%block>
diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html
index cea96cb755..5902e0fb50 100644
--- a/cms/templates/widgets/header.html
+++ b/cms/templates/widgets/header.html
@@ -42,7 +42,7 @@
@@ -84,7 +84,10 @@
diff --git a/cms/urls.py b/cms/urls.py
index e5c50afdd1..87875b050c 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -43,6 +43,7 @@ urlpatterns = ('',
url(r'^(?P
[^/]+)/(?P[^/]+)/info/(?P[^/]+)$', 'contentstore.views.course_info', name='course_info'),
url(r'^(?P[^/]+)/(?P[^/]+)/course_info/updates/(?P.*)$', 'contentstore.views.course_info_updates', name='course_info'),
url(r'^(?P[^/]+)/(?P[^/]+)/settings/(?P[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
+ url(r'^(?P[^/]+)/(?P[^/]+)/grades/(?P[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'),
url(r'^(?P[^/]+)/(?P[^/]+)/settings/(?P[^/]+)/section/(?P[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
url(r'^(?P[^/]+)/(?P[^/]+)/grades/(?P[^/]+)/(?P.*)$', 'contentstore.views.course_grader_updates', name='course_settings'),