Grading and details split out. Put the unimplemented tabs into a single
file not used.
This commit is contained in:
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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 <video youtube="speed:key, *"/> 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 <video youtube="speed:key, *"/> 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 "";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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('<span class="message-error"><%= message %></span>'),
|
||||
|
||||
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('<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>');
|
||||
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('<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>');
|
||||
// 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('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable>' +
|
||||
'<%= descriptor %>' +
|
||||
'</span><span class="range"></span>' +
|
||||
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
|
||||
'</li>');
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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
|
||||
<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/models/course_relative.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/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.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/models/settings/course_details.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
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})
|
||||
});
|
||||
|
||||
var editor = new CMS.Views.Settings.Main({
|
||||
el: $('.main-wrapper'),
|
||||
model : settingsModel
|
||||
var editor = new CMS.Views.Settings.Details({
|
||||
el: $('.settings-details'),
|
||||
model: new CMS.Models.Settings.CourseDetails(${course_details|n},{parse:true})
|
||||
});
|
||||
|
||||
editor.render();
|
||||
@@ -48,17 +42,6 @@ from contentstore import utils
|
||||
<div class="inner-wrapper">
|
||||
<h1>Settings</h1>
|
||||
<article class="settings-overview">
|
||||
<div class="sidebar">
|
||||
<nav class="settings-page-menu">
|
||||
<ul>
|
||||
<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>
|
||||
<!-- <li><a href="#" data-section="faculty">Faculty</a></li> -->
|
||||
<li><a href="#" data-section="grading">Grading</a></li>
|
||||
<!-- <li><a href="#" data-section="problems">Problems</a></li> -->
|
||||
<!-- <li><a href="#" data-section="discussions">Discussions</a></li> -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="settings-page-section main-column">
|
||||
|
||||
<section class="settings-details is-shown">
|
||||
@@ -75,7 +58,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(course_location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,7 +68,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(course_location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +78,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(course_location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,7 +198,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<textarea class="long tall tinymce text-editor" id="course-overview"></textarea>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course summary page</a></span>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,470 +242,6 @@ from contentstore import utils
|
||||
</section>
|
||||
</section><!-- .settings-details -->
|
||||
|
||||
<section class="settings-faculty">
|
||||
<h2 class="title">Faculty</h2>
|
||||
|
||||
<section class="settings-faculty-members">
|
||||
<header>
|
||||
<h3>Faculty Members</h3>
|
||||
<span class="detail">Individuals instructing and help with this course</span>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-faculty-list">
|
||||
<li class="input input-existing multi course-faculty-list-item">
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-firstname">Faculty First Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-1-firstname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-lastname">Faculty Last Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-1-lastname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-photo">Faculty Photo</label>
|
||||
<div class="field">
|
||||
<div class="input input-existing">
|
||||
<div class="current current-faculty-1-photo">
|
||||
<a href="#" class="remove-item remove-faculty-photo remove-video-data"><span class="delete-icon"></span> Delete Faculty Photo</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="course-faculty-1-bio">Faculty Bio:</label>
|
||||
<div class="field">
|
||||
<textarea class="long tall edit-box tinymce" id="course-faculty-1-bio"></textarea>
|
||||
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-faculty-data"><span class="delete-icon"></span> Delete Faculty Member</a>
|
||||
</li>
|
||||
|
||||
<li class="input multi course-faculty-list-item">
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-firstname">Faculty First Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-2-firstname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-lastname">Faculty Last Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-2-lastname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-photo">Faculty Photo</label>
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<a href="#" class="new-item new-faculty-photo add-faculty-photo-data" id="course-faculty-2-photo">
|
||||
<span class="upload-icon"></span>Upload Faculty Photo
|
||||
</a>
|
||||
<span class="tip tip-inline">Max size: 30KB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="course-faculty-2-bio">Faculty Bio:</label>
|
||||
<div class="field">
|
||||
<div clas="input">
|
||||
<textarea class="long tall edit-box tinymce" id="course-faculty-2-bio"></textarea>
|
||||
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-item new-course-faculty-item add-faculty-data">
|
||||
<span class="plus-icon"></span>New Faculty Member
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</section><!-- .settings-staff -->
|
||||
|
||||
<section class="settings-grading">
|
||||
<h2 class="title">Grading</h2>
|
||||
|
||||
<section class="settings-grading-range">
|
||||
<header>
|
||||
<h3>Overall Grade Range</h3>
|
||||
<span class="detail">Course grade ranges and their values</span>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="grade-controls course-grading-range well">
|
||||
<a href="#" class="new-grade-button"><span class="plus-icon"></span></a>
|
||||
<div class="grade-slider">
|
||||
<div class="grade-bar">
|
||||
<ol class="increments">
|
||||
<li class="increment-0">0</li>
|
||||
<li class="increment-10">10</li>
|
||||
<li class="increment-20">20</li>
|
||||
<li class="increment-30">30</li>
|
||||
<li class="increment-40">40</li>
|
||||
<li class="increment-50">50</li>
|
||||
<li class="increment-60">60</li>
|
||||
<li class="increment-70">70</li>
|
||||
<li class="increment-80">80</li>
|
||||
<li class="increment-90">90</li>
|
||||
<li class="increment-100">100</li>
|
||||
</ol>
|
||||
<ol class="grades">
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-grading-general">
|
||||
<header>
|
||||
<h3>General Grading</h3>
|
||||
<span class="detail">Deadlines and Requirements</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-grading-graceperiod">Grace Period on Deadline:</label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="e.g. 10 minutes">
|
||||
<span class="tip tip-inline">leeway on due dates</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="setting-grading-assignment-types">
|
||||
<header>
|
||||
<h3>Assignment Types</h3>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-grading-assignment-list">
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-button new-course-grading-item add-grading-data">
|
||||
<span class="plus-icon white"></span>New Assignment Type
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section><!-- .settings-grading -->
|
||||
|
||||
<section class="settings-problems">
|
||||
<h2 class="title">Problems</h2>
|
||||
|
||||
<section class="settings-problems-general">
|
||||
<header>
|
||||
<h3>General Settings</h3>
|
||||
<span class="detail">Course-wide settings for all problems</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Problem Randomization:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-always">Always</label>
|
||||
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-never">Never</label>
|
||||
<span class="tip tip-stacked"><strong>do not randomize</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-perstudent" value="Per Student">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-perstudent">Per Student</label>
|
||||
<span class="tip tip-stacked">randomize problems <strong>per student</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Show Answers:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-general-showanswer" id="course-problems-general-showanswer-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-showanswer-always">Always</label>
|
||||
<span class="tip tip-stacked">Answers will be shown after the number of attempts has been met</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-showanswer" id="course-problems-general-showanswer-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-showanswer-never">Never</label>
|
||||
<span class="tip tip-stacked">Answers will never be shown, regardless of attempts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="pcourse-roblems-general-attempts">Number of Attempts <br /> Allowed on Problems: </label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-problems-general-attempts" placeholder="0 or higher" value="0">
|
||||
<span class="tip tip-stacked">Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-problems-general -->
|
||||
|
||||
<section class="settings-problems-assignment-1 settings-extras">
|
||||
<header>
|
||||
<h3>[Assignment Type Name]</h3>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Problem Randomization:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-always">Always</label>
|
||||
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-never">Never</label>
|
||||
<span class="tip tip-stacked"><strong>do not randomize</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-perstudent" value="Per Student">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-perstudent">Per Student</label>
|
||||
<span class="tip tip-stacked">randomize problems <strong>per student</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Show Answers:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-assignment-1-showanswer" id="course-problems-assignment-1-showanswer-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-showanswer-always">Always</label>
|
||||
<span class="tip tip-stacked">Answers will be shown after the number of attempts has been met</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-showanswer" id="course-problems-assignment-1-showanswer-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="pcourse-roblems-assignment-1-showanswer-never">Never</label>
|
||||
<span class="tip tip-stacked">Answers will never be shown, regardless of attempts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-problems-assignment-1-attempts">Number of Attempts <br /> Allowed on Problems: </label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-problems-assignment-1-attempts" placeholder="0 or higher" value="0">
|
||||
<span class="tip tip-stacked">Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-problems-assignment-1 -->
|
||||
</section><!-- .settings-problems -->
|
||||
|
||||
<section class="settings-discussions">
|
||||
<h2 class="title">Discussions</h2>
|
||||
|
||||
<section class="settings-discussions-general">
|
||||
<header>
|
||||
<h3>General Settings</h3>
|
||||
<span class="detail">Course-wide settings for online discussion</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Anonymous Discussions:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-allow">Allow</label>
|
||||
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-dontallow">Do not allow</label>
|
||||
<span class="tip tip-stacked"><strong>Posting anonymously is not allowed</strong>. Any previous anonymous posts <strong>will be reverted to non-anonymous</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Anonymous Discussions:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-allow">Allow</label>
|
||||
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input disabled="disabled" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-dontallow">Do not allow</label>
|
||||
<span class="tip tip-stacked">This option is disabled since there are previous discussions that are anonymous.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Discussion Categories</h4>
|
||||
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-discussions-categories-list sortable">
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-1-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-1-name" placeholder="" value="General" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-2-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-2-name" placeholder="" value="Feedback" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-3-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-3-name" placeholder="" value="Troubleshooting" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-4-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-4-name" placeholder="" value="Study Groups">
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-5-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-5-name" placeholder="" value="Lectures">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-6-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-6-name" placeholder="" value="Labs">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-6-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-6-name" placeholder="" value="">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-item new-course-discussions-categories-item add-categories-data">
|
||||
<span class="plus-icon"></span>New Discussion Category
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-discussions-general -->
|
||||
</section><!-- .settings-discussions -->
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
430
cms/templates/settings_discussions_faculty.html
Normal file
430
cms/templates/settings_discussions_faculty.html
Normal file
@@ -0,0 +1,430 @@
|
||||
<!-- NOTE not used currently but retained b/c it's yet-to-be-wired functionality -->
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Schedule and details</%block>
|
||||
<%block name="bodyclass">is-signedin course settings</%block>
|
||||
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
|
||||
<%block name="jsextra">
|
||||
<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/views/validating_view.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
});
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<!-- -->
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>Settings</h1>
|
||||
<article class="settings-overview">
|
||||
<div class="settings-page-section main-column">
|
||||
|
||||
<section class="settings-faculty">
|
||||
<h2 class="title">Faculty</h2>
|
||||
|
||||
<section class="settings-faculty-members">
|
||||
<header>
|
||||
<h3>Faculty Members</h3>
|
||||
<span class="detail">Individuals instructing and help with this course</span>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-faculty-list">
|
||||
<li class="input input-existing multi course-faculty-list-item">
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-firstname">Faculty First Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-1-firstname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-lastname">Faculty Last Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-1-lastname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-1-photo">Faculty Photo</label>
|
||||
<div class="field">
|
||||
<div class="input input-existing">
|
||||
<div class="current current-faculty-1-photo">
|
||||
<a href="#" class="remove-item remove-faculty-photo remove-video-data"><span class="delete-icon"></span> Delete Faculty Photo</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="course-faculty-1-bio">Faculty Bio:</label>
|
||||
<div class="field">
|
||||
<textarea class="long tall edit-box tinymce" id="course-faculty-1-bio"></textarea>
|
||||
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-faculty-data"><span class="delete-icon"></span> Delete Faculty Member</a>
|
||||
</li>
|
||||
|
||||
<li class="input multi course-faculty-list-item">
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-firstname">Faculty First Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-2-firstname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-lastname">Faculty Last Name:</label>
|
||||
<div class="field">
|
||||
<input type="text" class="long" id="course-faculty-2-lastname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-faculty-2-photo">Faculty Photo</label>
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<a href="#" class="new-item new-faculty-photo add-faculty-photo-data" id="course-faculty-2-photo">
|
||||
<span class="upload-icon"></span>Upload Faculty Photo
|
||||
</a>
|
||||
<span class="tip tip-inline">Max size: 30KB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="course-faculty-2-bio">Faculty Bio:</label>
|
||||
<div class="field">
|
||||
<div clas="input">
|
||||
<textarea class="long tall edit-box tinymce" id="course-faculty-2-bio"></textarea>
|
||||
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-item new-course-faculty-item add-faculty-data">
|
||||
<span class="plus-icon"></span>New Faculty Member
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</section><!-- .settings-staff -->
|
||||
|
||||
<section class="settings-problems">
|
||||
<h2 class="title">Problems</h2>
|
||||
|
||||
<section class="settings-problems-general">
|
||||
<header>
|
||||
<h3>General Settings</h3>
|
||||
<span class="detail">Course-wide settings for all problems</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Problem Randomization:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-always">Always</label>
|
||||
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-never">Never</label>
|
||||
<span class="tip tip-stacked"><strong>do not randomize</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-perstudent" value="Per Student">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-perstudent">Per Student</label>
|
||||
<span class="tip tip-stacked">randomize problems <strong>per student</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Show Answers:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-general-showanswer" id="course-problems-general-showanswer-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-showanswer-always">Always</label>
|
||||
<span class="tip tip-stacked">Answers will be shown after the number of attempts has been met</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-general-showanswer" id="course-problems-general-showanswer-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-showanswer-never">Never</label>
|
||||
<span class="tip tip-stacked">Answers will never be shown, regardless of attempts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="pcourse-roblems-general-attempts">Number of Attempts <br /> Allowed on Problems: </label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-problems-general-attempts" placeholder="0 or higher" value="0">
|
||||
<span class="tip tip-stacked">Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-problems-general -->
|
||||
|
||||
<section class="settings-problems-assignment-1 settings-extras">
|
||||
<header>
|
||||
<h3>[Assignment Type Name]</h3>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Problem Randomization:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-always">Always</label>
|
||||
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-never">Never</label>
|
||||
<span class="tip tip-stacked"><strong>do not randomize</strong> problems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-perstudent" value="Per Student">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-perstudent">Per Student</label>
|
||||
<span class="tip tip-stacked">randomize problems <strong>per student</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Show Answers:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-assignment-1-showanswer" id="course-problems-assignment-1-showanswer-always" value="Always">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-showanswer-always">Always</label>
|
||||
<span class="tip tip-stacked">Answers will be shown after the number of attempts has been met</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-problems-assignment-1-showanswer" id="course-problems-assignment-1-showanswer-never" value="Never">
|
||||
|
||||
<div class="copy">
|
||||
<label for="pcourse-roblems-assignment-1-showanswer-never">Never</label>
|
||||
<span class="tip tip-stacked">Answers will never be shown, regardless of attempts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-problems-assignment-1-attempts">Number of Attempts <br /> Allowed on Problems: </label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-problems-assignment-1-attempts" placeholder="0 or higher" value="0">
|
||||
<span class="tip tip-stacked">Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-problems-assignment-1 -->
|
||||
</section><!-- .settings-problems -->
|
||||
|
||||
<section class="settings-discussions">
|
||||
<h2 class="title">Discussions</h2>
|
||||
|
||||
<section class="settings-discussions-general">
|
||||
<header>
|
||||
<h3>General Settings</h3>
|
||||
<span class="detail">Course-wide settings for online discussion</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Anonymous Discussions:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-allow">Allow</label>
|
||||
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-dontallow">Do not allow</label>
|
||||
<span class="tip tip-stacked"><strong>Posting anonymously is not allowed</strong>. Any previous anonymous posts <strong>will be reverted to non-anonymous</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Anonymous Discussions:</h4>
|
||||
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-allow">Allow</label>
|
||||
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input input-radio">
|
||||
<input disabled="disabled" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow">
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-dontallow">Do not allow</label>
|
||||
<span class="tip tip-stacked">This option is disabled since there are previous discussions that are anonymous.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-col2">
|
||||
<h4 class="label">Discussion Categories</h4>
|
||||
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-discussions-categories-list sortable">
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-1-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-1-name" placeholder="" value="General" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-2-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-2-name" placeholder="" value="Feedback" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-3-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-3-name" placeholder="" value="Troubleshooting" disabled="disabled">
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-4-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-4-name" placeholder="" value="Study Groups">
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
</div>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-5-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-5-name" placeholder="" value="Lectures">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-6-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-6-name" placeholder="" value="Labs">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
<li class="input input-existing course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-6-name">Category Name: </label>
|
||||
<input type="text" class="course-discussions-categories-name" id="course-discussions-categories-6-name" placeholder="" value="">
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-course-discussions-categories-data"><span class="delete-icon"></span> Delete Category</a>
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-item new-course-discussions-categories-item add-categories-data">
|
||||
<span class="plus-icon"></span>New Discussion Category
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section><!-- .settings-discussions-general -->
|
||||
</section><!-- .settings-discussions -->
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<footer></footer>
|
||||
</%block>
|
||||
123
cms/templates/settings_graders.html
Normal file
123
cms/templates/settings_graders.html
Normal file
@@ -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">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.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/views/validating_view.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/settings_grading_view.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
var editor = new CMS.Views.Settings.Grading({
|
||||
el: $('.settings-grading'),
|
||||
model : new CMS.Models.Settings.CourseGradingPolicy(${course_details|n},{parse:true})
|
||||
});
|
||||
|
||||
editor.render();
|
||||
});
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<!-- -->
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>Settings</h1>
|
||||
<article class="settings-overview">
|
||||
<div class="settings-page-section main-column">
|
||||
|
||||
<section class="settings-grading is-shown">
|
||||
<h2 class="title">Grading</h2>
|
||||
|
||||
<section class="settings-grading-range">
|
||||
<header>
|
||||
<h3>Overall Grade Range</h3>
|
||||
<span class="detail">Course grade ranges and their values</span>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="grade-controls course-grading-range well">
|
||||
<a href="#" class="new-grade-button"><span class="plus-icon"></span></a>
|
||||
<div class="grade-slider">
|
||||
<div class="grade-bar">
|
||||
<ol class="increments">
|
||||
<li class="increment-0">0</li>
|
||||
<li class="increment-10">10</li>
|
||||
<li class="increment-20">20</li>
|
||||
<li class="increment-30">30</li>
|
||||
<li class="increment-40">40</li>
|
||||
<li class="increment-50">50</li>
|
||||
<li class="increment-60">60</li>
|
||||
<li class="increment-70">70</li>
|
||||
<li class="increment-80">80</li>
|
||||
<li class="increment-90">90</li>
|
||||
<li class="increment-100">100</li>
|
||||
</ol>
|
||||
<ol class="grades">
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-grading-general">
|
||||
<header>
|
||||
<h3>General Grading</h3>
|
||||
<span class="detail">Deadlines and Requirements</span>
|
||||
</header>
|
||||
|
||||
<div class="row row-col2">
|
||||
<label for="course-grading-graceperiod">Grace Period on Deadline:</label>
|
||||
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="e.g. 10 minutes">
|
||||
<span class="tip tip-inline">leeway on due dates</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="setting-grading-assignment-types">
|
||||
<header>
|
||||
<h3>Assignment Types</h3>
|
||||
</header>
|
||||
|
||||
<div class="row">
|
||||
<div class="field enum">
|
||||
<ul class="input-list course-grading-assignment-list">
|
||||
</ul>
|
||||
|
||||
<a href="#" class="new-button new-course-grading-item add-grading-data">
|
||||
<span class="plus-icon white"></span>New Assignment Type
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section><!-- .settings-grading -->
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<footer></footer>
|
||||
</%block>
|
||||
@@ -42,7 +42,7 @@
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Schedule & Details</a></li>
|
||||
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li>
|
||||
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li>
|
||||
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
|
||||
<li class="nav-item"><a href="${reverse('course_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li>
|
||||
</ul>
|
||||
@@ -84,7 +84,10 @@
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
% if context_course:
|
||||
<% ctx_loc = context_course.location %>
|
||||
<li class="nav-item"><a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">My Courses</a></li>
|
||||
% endif
|
||||
<li class="nav-item"><a href="http://help.edge.edx.org/" rel="external">Help</a></li>
|
||||
<li class="nav-item"><a class="action action-logout" href="${reverse('logout')}">Logout</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -43,6 +43,7 @@ urlpatterns = ('',
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$', 'contentstore.views.course_info', name='course_info'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/grades/(?P<name>[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/grades/(?P<name>[^/]+)/(?P<grader_index>.*)$', 'contentstore.views.course_grader_updates', name='course_settings'),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user