');
+ // cdodge: this looks funny, but on AWS instances, this base.js get's wrapped in a separate scope as part of Django static
+ // pipelining (note, this doesn't happen on local runtimes). So if we set it on window, when we can access it from other
+ // scopes (namely the course-info tab)
+ window.$modalCover = $modalCover;
+
+ // Control whether template caching in local memory occurs (see template_loader.js). Caching screws up development but may
+ // be a good optimization in production (it works fairly well)
+ window.cachetemplates = false;
+
$body.append($modalCover);
$newComponentItem = $('.new-component-item');
$newComponentTypePicker = $('.new-component');
@@ -34,6 +41,8 @@ $(document).ready(function() {
$('.unit .item-actions .delete-button').bind('click', deleteUnit);
$('.new-unit-item').bind('click', createNewUnit);
+ $('.collapse-all-button').bind('click', collapseAll);
+
// autosave when a field is updated on the subsection page
$body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue);
$('.subsection-display-name-input, .unit-subtitle, .policy-list-name, .policy-list-value').each(function(i) {
@@ -100,15 +109,12 @@ $(document).ready(function() {
$('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate);
$('.edit-section-start-save').bind('click', saveSetSectionScheduleDate);
- // modal upload asset dialog. Bind it in the initializer otherwise multiple hanlders will get registered causing
- // pretty wacky stuff to happen
- $('.file-input').bind('change', startUpload);
$('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu);
$body.on('click', '.section-published-date .edit-button', editSectionPublishDate);
$body.on('click', '.section-published-date .schedule-button', editSectionPublishDate);
$body.on('click', '.edit-subsection-publish-settings .save-button', saveSetSectionScheduleDate);
- $body.on('click', '.edit-subsection-publish-settings .cancel-button', hideModal)
+ $body.on('click', '.edit-subsection-publish-settings .cancel-button', hideModal);
$body.on('change', '.edit-subsection-publish-settings .start-date', function() {
if($('.edit-subsection-publish-settings').find('.start-time').val() == '') {
$('.edit-subsection-publish-settings').find('.start-time').val('12:00am');
@@ -119,6 +125,11 @@ $(document).ready(function() {
});
});
+function collapseAll(e) {
+ $('.branch').addClass('collapsed');
+ $('.expand-collapse-icon').removeClass('collapse').addClass('expand');
+}
+
function editSectionPublishDate(e) {
e.preventDefault();
$modal = $('.edit-subsection-publish-settings').show();
@@ -298,7 +309,7 @@ function checkForNewValue(e) {
this.saveTimer = setTimeout(function() {
$changedInput = $(e.target);
- saveSubsection()
+ saveSubsection();
this.saveTimer = null;
}, 500);
}
@@ -311,7 +322,7 @@ function autosaveInput(e) {
this.saveTimer = setTimeout(function() {
$changedInput = $(e.target);
- saveSubsection()
+ saveSubsection();
this.saveTimer = null;
}, 500);
}
@@ -333,23 +344,22 @@ function saveSubsection() {
// pull all 'normalized' metadata editable fields on page
var metadata_fields = $('input[data-metadata-name]');
- metadata = {};
+ var metadata = {};
for(var i=0; i< metadata_fields.length;i++) {
- el = metadata_fields[i];
+ var el = metadata_fields[i];
metadata[$(el).data("metadata-name")] = el.value;
}
// now add 'free-formed' metadata which are presented to the user as dual input fields (name/value)
$('ol.policy-list > li.policy-list-element').each( function(i, element) {
- name = $(element).children('.policy-list-name').val();
- val = $(element).children('.policy-list-value').val();
- metadata[name] = val;
+ var name = $(element).children('.policy-list-name').val();
+ metadata[name] = $(element).children('.policy-list-value').val();
});
// now add any 'removed' policy metadata which is stored in a separate hidden div
// 'null' presented to the server means 'remove'
$("#policy-to-delete > li.policy-list-element").each(function(i, element) {
- name = $(element).children('.policy-list-name').val();
+ var name = $(element).children('.policy-list-name').val();
if (name != "")
metadata[name] = null;
});
@@ -385,7 +395,7 @@ function createNewUnit(e) {
$.post('/clone_item',
{'parent_location' : parent,
'template' : template,
- 'display_name': 'New Unit',
+ 'display_name': 'New Unit'
},
function(data) {
// redirect to the edit page
@@ -475,7 +485,7 @@ function displayFinishedUpload(xhr) {
var template = $('#new-asset-element').html();
var html = Mustache.to_html(template, resp);
- $('table > tbody > tr:first').before(html);
+ $('table > tbody').prepend(html);
}
@@ -488,6 +498,7 @@ function hideModal(e) {
if(e) {
e.preventDefault();
}
+ $('.file-input').unbind('change', startUpload);
$modal.hide();
$modalCover.hide();
}
@@ -588,9 +599,11 @@ function hideToastMessage(e) {
function addNewSection(e, isTemplate) {
e.preventDefault();
+ $(e.target).addClass('disabled');
+
var $newSection = $($('#new-section-template').html());
var $cancelButton = $newSection.find('.new-section-name-cancel');
- $('.new-courseware-section-button').after($newSection);
+ $('.courseware-overview').prepend($newSection);
$newSection.find('.new-section-name').focus().select();
$newSection.find('.section-name-form').bind('submit', saveNewSection);
$cancelButton.bind('click', cancelNewSection);
@@ -627,11 +640,14 @@ function saveNewSection(e) {
function cancelNewSection(e) {
e.preventDefault();
+ $('.new-courseware-section-button').removeClass('disabled');
$(this).parents('section.new-section').remove();
}
function addNewCourse(e) {
e.preventDefault();
+
+ $(e.target).hide();
var $newCourse = $($('#new-course-template').html());
var $cancelButton = $newCourse.find('.new-course-cancel');
$('.new-course-button').after($newCourse);
@@ -659,7 +675,7 @@ function saveNewCourse(e) {
'template' : template,
'org' : org,
'number' : number,
- 'display_name': display_name,
+ 'display_name': display_name
},
function(data) {
if (data.id != undefined) {
@@ -672,6 +688,7 @@ function saveNewCourse(e) {
function cancelNewCourse(e) {
e.preventDefault();
+ $('.new-course-button').show();
$(this).parents('section.new-course').remove();
}
@@ -687,7 +704,7 @@ function addNewSubsection(e) {
var parent = $(this).parents("section.branch").data("id");
- $saveButton.data('parent', parent)
+ $saveButton.data('parent', parent);
$saveButton.data('template', $(this).data('template'));
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
@@ -752,7 +769,7 @@ function saveEditSectionName(e) {
$spinner.show();
if (display_name == '') {
- alert("You must specify a name before saving.")
+ alert("You must specify a name before saving.");
return;
}
@@ -789,13 +806,12 @@ function cancelSetSectionScheduleDate(e) {
function saveSetSectionScheduleDate(e) {
e.preventDefault();
- input_date = $('.edit-subsection-publish-settings .start-date').val();
- input_time = $('.edit-subsection-publish-settings .start-time').val();
+ var input_date = $('.edit-subsection-publish-settings .start-date').val();
+ var input_time = $('.edit-subsection-publish-settings .start-time').val();
- start = getEdxTimeFromDateTimeVals(input_date, input_time);
+ var start = getEdxTimeFromDateTimeVals(input_date, input_time);
- id = $modal.attr('data-id');
- var $_this = $(this);
+ var id = $modal.attr('data-id');
// call into server to commit the new order
$.ajax({
diff --git a/cms/static/js/models/course_info.js b/cms/static/js/models/course_info.js
new file mode 100644
index 0000000000..8cb5a654cb
--- /dev/null
+++ b/cms/static/js/models/course_info.js
@@ -0,0 +1,36 @@
+// single per course holds the updates and handouts
+CMS.Models.CourseInfo = Backbone.Model.extend({
+ // This model class is not suited for restful operations and is considered just a server side initialized container
+ url: '',
+
+ defaults: {
+ "courseId": "", // the location url
+ "updates" : null, // UpdateCollection
+ "handouts": null // HandoutCollection
+ },
+
+ idAttribute : "courseId"
+});
+
+// course update -- biggest kludge here is the lack of a real id to map updates to originals
+CMS.Models.CourseUpdate = Backbone.Model.extend({
+ defaults: {
+ "date" : $.datepicker.formatDate('MM d, yy', new Date()),
+ "content" : ""
+ }
+});
+
+/*
+ The intitializer of this collection must set id to the update's location.url and courseLocation to the course's location. Must pass the
+ collection of updates as [{ date : "month day", content : "html"}]
+*/
+CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
+ url : function() {return this.urlbase + "course_info/updates/";},
+
+ model : CMS.Models.CourseUpdate
+});
+
+
+
+
+
\ No newline at end of file
diff --git a/cms/static/js/models/course_relative.js b/cms/static/js/models/course_relative.js
new file mode 100644
index 0000000000..99bb1c6d77
--- /dev/null
+++ b/cms/static/js/models/course_relative.js
@@ -0,0 +1,68 @@
+CMS.Models.Location = Backbone.Model.extend({
+ defaults: {
+ tag: "",
+ org: "",
+ course: "",
+ category: "",
+ name: ""
+ },
+ toUrl: function(overrides) {
+ return
+ (overrides && overrides['tag'] ? overrides['tag'] : this.get('tag')) + "://" +
+ (overrides && overrides['org'] ? overrides['org'] : this.get('org')) + "/" +
+ (overrides && overrides['course'] ? overrides['course'] : this.get('course')) + "/" +
+ (overrides && overrides['category'] ? overrides['category'] : this.get('category')) + "/" +
+ (overrides && overrides['name'] ? overrides['name'] : this.get('name')) + "/";
+ },
+ _tagPattern : /[^:]+/g,
+ _fieldPattern : new RegExp('[^/]+','g'),
+
+ parse: function(payload) {
+ if (_.isArray(payload)) {
+ return {
+ tag: payload[0],
+ org: payload[1],
+ course: payload[2],
+ category: payload[3],
+ name: payload[4]
+ }
+ }
+ else if (_.isString(payload)) {
+ this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes
+ var foundTag = this._tagPattern.exec(payload);
+ if (foundTag) {
+ this._fieldPattern.lastIndex = this._tagPattern.lastIndex + 1; // skip over the colon
+ return {
+ tag: foundTag[0],
+ org: this.getNextField(payload),
+ course: this.getNextField(payload),
+ category: this.getNextField(payload),
+ name: this.getNextField(payload)
+ }
+ }
+ else return null;
+ }
+ else {
+ return payload;
+ }
+ },
+ getNextField : function(payload) {
+ try {
+ return this._fieldPattern.exec(payload)[0];
+ }
+ catch (err) {
+ return "";
+ }
+ }
+});
+
+CMS.Models.CourseRelative = Backbone.Model.extend({
+ defaults: {
+ course_location : null, // must never be null, but here to doc the field
+ idx : null // the index making it unique in the containing collection (no implied sort)
+ }
+});
+
+CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({
+ model : CMS.Models.CourseRelative
+});
\ No newline at end of file
diff --git a/cms/static/js/models/module_info.js b/cms/static/js/models/module_info.js
new file mode 100644
index 0000000000..6a593372c4
--- /dev/null
+++ b/cms/static/js/models/module_info.js
@@ -0,0 +1,10 @@
+CMS.Models.ModuleInfo = Backbone.Model.extend({
+ url: function() {return "/module_info/" + this.id;},
+
+ defaults: {
+ "id": null,
+ "data": null,
+ "metadata" : null,
+ "children" : null
+ },
+});
\ No newline at end of file
diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js
new file mode 100644
index 0000000000..ab80179142
--- /dev/null
+++ b/cms/static/js/models/settings/course_details.js
@@ -0,0 +1,83 @@
+if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
+
+CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
+ defaults: {
+ location : null, // the course's Location model, required
+ start_date: null, // maps to 'start'
+ end_date: null, // maps to 'end'
+ enrollment_start: null,
+ enrollment_end: null,
+ syllabus: null,
+ overview: "",
+ intro_video: null,
+ effort: null // an int or null
+ },
+
+ // When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
+ parse: function(attributes) {
+ if (attributes['course_location']) {
+ attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
+ }
+ if (attributes['start_date']) {
+ attributes.start_date = new Date(attributes.start_date);
+ }
+ if (attributes['end_date']) {
+ attributes.end_date = new Date(attributes.end_date);
+ }
+ if (attributes['enrollment_start']) {
+ attributes.enrollment_start = new Date(attributes.enrollment_start);
+ }
+ if (attributes['enrollment_end']) {
+ attributes.enrollment_end = new Date(attributes.enrollment_end);
+ }
+ return attributes;
+ },
+
+ validate: function(newattrs) {
+ // Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
+ // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
+ var errors = {};
+ if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
+ errors.end_date = "The course end date cannot be before the course start date.";
+ }
+ if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
+ errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
+ }
+ if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
+ errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
+ }
+ if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
+ errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
+ }
+ if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
+ if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
+ errors.intro_video = "Key should only contain letters, numbers, _, or -";
+ }
+ // TODO check if key points to a real video using google's youtube api
+ }
+ if (!_.isEmpty(errors)) return errors;
+ // NOTE don't return empty errors as that will be interpreted as an error state
+ },
+
+ url: function() {
+ var location = this.get('location');
+ return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
+ },
+
+ _videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
+ save_videosource: function(newsource) {
+ // newsource either is
or just the "speed:key, *" string
+ // returns the videosource for the preview which iss the key whose speed is closest to 1
+ if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
+ // TODO remove all whitespace w/in string
+ else {
+ if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
+ }
+
+ return this.videosourceSample();
+ },
+ videosourceSample : function() {
+ if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
+ else return "";
+ }
+});
diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js
new file mode 100644
index 0000000000..cce4e0207d
--- /dev/null
+++ b/cms/static/js/models/settings/course_grading_policy.js
@@ -0,0 +1,127 @@
+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
+ 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() };
+ }
+});
+
+CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
+ defaults: {
+ "type" : "", // must be unique w/in collection (ie. w/in course)
+ "min_count" : 1,
+ "drop_count" : 0,
+ "short_label" : "", // what to use in place of type if space is an issue
+ "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;
+ },
+ 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;
+ }
+});
+
+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);
+ }
+});
\ No newline at end of file
diff --git a/cms/static/js/models/settings/course_settings.js b/cms/static/js/models/settings/course_settings.js
new file mode 100644
index 0000000000..9d09e4bdc5
--- /dev/null
+++ b/cms/static/js/models/settings/course_settings.js
@@ -0,0 +1,43 @@
+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
+ },
+
+ 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;
+ }
+ }
+ }
+})
\ No newline at end of file
diff --git a/cms/static/js/template_loader.js b/cms/static/js/template_loader.js
new file mode 100644
index 0000000000..89ecc98cc4
--- /dev/null
+++ b/cms/static/js/template_loader.js
@@ -0,0 +1,78 @@
+//
+// TODO Figure out how to initialize w/ static views from server (don't call .load but instead inject in django as strings)
+// so this only loads the lazily loaded ones.
+(function() {
+ if (typeof window.templateLoader == 'function') return;
+
+ var templateLoader = {
+ templateVersion: "0.0.12",
+ templates: {},
+ loadRemoteTemplate: function(templateName, filename, callback) {
+ if (!this.templates[templateName]) {
+ var self = this;
+ jQuery.ajax({url : filename,
+ success : function(data) {
+ self.addTemplate(templateName, data);
+ self.saveLocalTemplates();
+ callback(data);
+ },
+ error : function(xhdr, textStatus, errorThrown) {
+ console.log(textStatus); },
+ dataType : "html"
+ })
+ }
+ else {
+ callback(this.templates[templateName]);
+ }
+ },
+
+ addTemplate: function(templateName, data) {
+ // is there a reason this doesn't go ahead and compile the template? _.template(data)
+ // I suppose localstorage use would still req raw string rather than compiled version, but that sd work
+ // if it maintains a separate cache of uncompiled ones
+ this.templates[templateName] = data;
+ },
+
+ localStorageAvailable: function() {
+ try {
+ // window.cachetemplates is global set in base.js to turn caching on/off
+ return window.cachetemplates && 'localStorage' in window && window['localStorage'] !== null;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ saveLocalTemplates: function() {
+ if (this.localStorageAvailable) {
+ localStorage.setItem("templates", JSON.stringify(this.templates));
+ localStorage.setItem("templateVersion", this.templateVersion);
+ }
+ },
+
+ loadLocalTemplates: function() {
+ if (this.localStorageAvailable) {
+ var templateVersion = localStorage.getItem("templateVersion");
+ if (templateVersion && templateVersion == this.templateVersion) {
+ var templates = localStorage.getItem("templates");
+ if (templates) {
+ templates = JSON.parse(templates);
+ for (var x in templates) {
+ if (!this.templates[x]) {
+ this.addTemplate(x, templates[x]);
+ }
+ }
+ }
+ }
+ else {
+ localStorage.removeItem("templates");
+ localStorage.removeItem("templateVersion");
+ }
+ }
+ }
+
+
+
+ };
+ templateLoader.loadLocalTemplates();
+ window.templateLoader = templateLoader;
+ })();
diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js
new file mode 100644
index 0000000000..0ad02215db
--- /dev/null
+++ b/cms/static/js/views/course_info_edit.js
@@ -0,0 +1,287 @@
+/* this view should own everything on the page which has controls effecting its operation
+ generate other views for the individual editors.
+ The render here adds views for each update/handout by delegating to their collections but does not
+ generate any html for the surrounding page.
+*/
+CMS.Views.CourseInfoEdit = Backbone.View.extend({
+ // takes CMS.Models.CourseInfo as model
+ tagName: 'div',
+
+ render: function() {
+ // instantiate the ClassInfoUpdateView and delegate the proper dom to it
+ new CMS.Views.ClassInfoUpdateView({
+ el: this.$('#course-update-view'),
+ collection: this.model.get('updates')
+ });
+
+ new CMS.Views.ClassInfoHandoutsView({
+ el: this.$('#course-handouts-view'),
+ model: this.model.get('handouts')
+ });
+ return this;
+ }
+});
+
+// ??? Programming style question: should each of these classes be in separate files?
+CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
+ // collection is CourseUpdateCollection
+ events: {
+ "click .new-update-button" : "onNew",
+ "click .save-button" : "onSave",
+ "click .cancel-button" : "onCancel",
+ "click .edit-button" : "onEdit",
+ "click .delete-button" : "onDelete"
+ },
+
+ initialize: function() {
+ var self = this;
+ // instantiates an editor template for each update in the collection
+ window.templateLoader.loadRemoteTemplate("course_info_update",
+ // TODO Where should the template reside? how to use the static.url to create the path?
+ "/static/client_templates/course_info_update.html",
+ function (raw_template) {
+ self.template = _.template(raw_template);
+ self.render();
+ }
+ );
+ },
+
+ render: function () {
+ // iterate over updates and create views for each using the template
+ var updateEle = this.$el.find("#course-update-list");
+ // remove and then add all children
+ $(updateEle).empty();
+ var self = this;
+ this.collection.each(function (update) {
+ var newEle = self.template({ updateModel : update });
+ $(updateEle).append(newEle);
+ });
+ this.$el.find(".new-update-form").hide();
+ this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
+ return this;
+ },
+
+ onNew: function(event) {
+ event.preventDefault();
+ var self = this;
+ // create new obj, insert into collection, and render this one ele overriding the hidden attr
+ var newModel = new CMS.Models.CourseUpdate();
+ this.collection.add(newModel, {at : 0});
+
+ var $newForm = $(this.template({ updateModel : newModel }));
+
+ var updateEle = this.$el.find("#course-update-list");
+ $(updateEle).prepend($newForm);
+
+ var $textArea = $newForm.find(".new-update-content").first();
+ if (this.$codeMirror == null ) {
+ this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
+ mode: "text/html",
+ lineNumbers: true,
+ lineWrapping: true,
+ });
+ }
+
+ $newForm.addClass('editing');
+ this.$currentPost = $newForm.closest('li');
+
+ window.$modalCover.show();
+ window.$modalCover.bind('click', function() {
+ self.closeEditor(self, true);
+ });
+
+ $('.date').datepicker('destroy');
+ $('.date').datepicker({ 'dateFormat': 'MM d, yy' });
+ },
+
+ onSave: function(event) {
+ event.preventDefault();
+ var targetModel = this.eventModel(event);
+ targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
+ // push change to display, hide the editor, submit the change
+ targetModel.save({}, {error : function(model, xhr) {
+ // TODO use a standard component
+ window.alert(xhr.responseText);
+ }});
+ this.closeEditor(this);
+ },
+
+ onCancel: function(event) {
+ event.preventDefault();
+ // change editor contents back to model values and hide the editor
+ $(this.editor(event)).hide();
+ var targetModel = this.eventModel(event);
+ this.closeEditor(this, !targetModel.id);
+ },
+
+ onEdit: function(event) {
+ event.preventDefault();
+ var self = this;
+ this.$currentPost = $(event.target).closest('li');
+ this.$currentPost.addClass('editing');
+
+ $(this.editor(event)).show();
+ var $textArea = this.$currentPost.find(".new-update-content").first();
+ if (this.$codeMirror == null ) {
+ this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
+ mode: "text/html",
+ lineNumbers: true,
+ lineWrapping: true,
+ });
+ }
+
+ window.$modalCover.show();
+ var targetModel = this.eventModel(event);
+ window.$modalCover.bind('click', function() {
+ self.closeEditor(self);
+ });
+ },
+
+ onDelete: function(event) {
+ event.preventDefault();
+ // TODO ask for confirmation
+ // remove the dom element and delete the model
+ var targetModel = this.eventModel(event);
+ this.modelDom(event).remove();
+ var cacheThis = this;
+ targetModel.destroy({success : function (model, response) {
+ cacheThis.collection.fetch({success : function() {cacheThis.render();}});
+ }
+ });
+ },
+
+ closeEditor: function(self, removePost) {
+ var targetModel = self.collection.getByCid(self.$currentPost.attr('name'));
+
+ if(removePost) {
+ self.$currentPost.remove();
+ }
+
+ // close the modal and insert the appropriate data
+ self.$currentPost.removeClass('editing');
+ self.$currentPost.find('.date-display').html(targetModel.get('date'));
+ self.$currentPost.find('.date').val(targetModel.get('date'));
+ self.$currentPost.find('.update-contents').html(targetModel.get('content'));
+ self.$currentPost.find('.new-update-content').val(targetModel.get('content'));
+ self.$currentPost.find('form').hide();
+ window.$modalCover.unbind('click');
+ window.$modalCover.hide();
+ this.$codeMirror = null;
+ self.$currentPost.find('.CodeMirror').remove();
+ },
+
+ // Dereferencing from events to screen elements
+ eventModel: function(event) {
+ // not sure if it should be currentTarget or delegateTarget
+ return this.collection.getByCid($(event.currentTarget).attr("name"));
+ },
+
+ modelDom: function(event) {
+ return $(event.currentTarget).closest("li");
+ },
+
+ editor: function(event) {
+ var li = $(event.currentTarget).closest("li");
+ if (li) return $(li).find("form").first();
+ },
+
+ dateEntry: function(event) {
+ var li = $(event.currentTarget).closest("li");
+ if (li) return $(li).find(".date").first();
+ },
+
+ contentEntry: function(event) {
+ return $(event.currentTarget).closest("li").find(".new-update-content").first();
+ },
+
+ dateDisplay: function(event) {
+ return $(event.currentTarget).closest("li").find("#date-display").first();
+ },
+
+ contentDisplay: function(event) {
+ return $(event.currentTarget).closest("li").find(".update-contents").first();
+ }
+
+});
+
+// the handouts view is dumb right now; it needs tied to a model and all that jazz
+CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
+ // collection is CourseUpdateCollection
+ events: {
+ "click .save-button" : "onSave",
+ "click .cancel-button" : "onCancel",
+ "click .edit-button" : "onEdit"
+ },
+
+ initialize: function() {
+ var self = this;
+ this.model.fetch(
+ {
+ complete: function() {
+ window.templateLoader.loadRemoteTemplate("course_info_handouts",
+ "/static/client_templates/course_info_handouts.html",
+ function (raw_template) {
+ self.template = _.template(raw_template);
+ self.render();
+ }
+ );
+ }
+ }
+ );
+ },
+
+ render: function () {
+ var updateEle = this.$el;
+ var self = this;
+ this.$el.html(
+ $(this.template( {
+ model: this.model
+ })
+ )
+ );
+ this.$preview = this.$el.find('.handouts-content');
+ this.$form = this.$el.find(".edit-handouts-form");
+ this.$editor = this.$form.find('.handouts-content-editor');
+ this.$form.hide();
+
+ return this;
+ },
+
+ onEdit: function(event) {
+ var self = this;
+ this.$editor.val(this.$preview.html());
+ this.$form.show();
+ if (this.$codeMirror == null) {
+ this.$codeMirror = CodeMirror.fromTextArea(this.$editor.get(0), {
+ mode: "text/html",
+ lineNumbers: true,
+ lineWrapping: true,
+ });
+ }
+ window.$modalCover.show();
+ window.$modalCover.bind('click', function() {
+ self.closeEditor(self);
+ });
+ },
+
+ onSave: function(event) {
+ this.model.set('data', this.$codeMirror.getValue());
+ this.render();
+ this.model.save();
+ this.$form.hide();
+ this.closeEditor(this);
+ },
+
+ onCancel: function(event) {
+ this.$form.hide();
+ this.closeEditor(this);
+ },
+
+ closeEditor: function(self) {
+ this.$form.hide();
+ window.$modalCover.unbind('click');
+ window.$modalCover.hide();
+ self.$form.find('.CodeMirror').remove();
+ this.$codeMirror = null;
+ }
+});
\ No newline at end of file
diff --git a/cms/static/js/views/grader-select-view.js b/cms/static/js/views/grader-select-view.js
new file mode 100644
index 0000000000..50e32cbbe1
--- /dev/null
+++ b/cms/static/js/views/grader-select-view.js
@@ -0,0 +1,90 @@
+CMS.Models.AssignmentGrade = Backbone.Model.extend({
+ defaults : {
+ graderType : null, // the type label (string). May be "Not Graded" which implies None. I'd like to use id but that's ephemeral
+ location : null // A location object
+ },
+ initialize : function(attrs) {
+ if (attrs['assignmentUrl']) {
+ this.set('location', new CMS.Models.Location(attrs['assignmentUrl'], {parse: true}));
+ }
+ },
+ parse : function(attrs) {
+ if (attrs && attrs['location']) {
+ attrs.location = new CMS.Models.Location(attrs['location'], {parse: true});
+ }
+ },
+ urlRoot : function() {
+ if (this.has('location')) {
+ var location = this.get('location');
+ return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
+ + location.get('name') + '/gradeas/';
+ }
+ else return "";
+ }
+});
+
+CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
+ // instantiate w/ { graders : CourseGraderCollection, el :
}
+ events : {
+ "click .menu-toggle" : "showGradeMenu",
+ "click .menu li" : "selectGradeType"
+ },
+ initialize : function() {
+ // call template w/ {assignmentType : formatname, graders : CourseGraderCollection instance }
+ this.template = _.template(
+ // TODO move to a template file
+ '<%= assignmentType %>
' +
+ '' +
+ '');
+ this.assignmentGrade = new CMS.Models.AssignmentGrade({
+ assignmentUrl : this.$el.closest('.id-holder').data('id'),
+ graderType : this.$el.data('initial-status')});
+ // TODO throw exception if graders is null
+ this.graders = this.options['graders'];
+ var cachethis = this;
+ // defining here to get closure around this
+ this.removeMenu = function(e) {
+ e.preventDefault();
+ cachethis.$el.removeClass('is-active');
+ $(document).off('click', cachethis.removeMenu);
+ }
+ this.hideSymbol = this.options['hideSymbol'];
+ this.render();
+ },
+ render : function() {
+ this.$el.html(this.template({ assignmentType : this.assignmentGrade.get('graderType'), graders : this.graders,
+ hideSymbol : this.hideSymbol }));
+ if (this.assignmentGrade.has('graderType') && this.assignmentGrade.get('graderType') != "Not Graded") {
+ this.$el.addClass('is-set');
+ }
+ else {
+ this.$el.removeClass('is-set');
+ }
+ },
+ showGradeMenu : function(e) {
+ e.preventDefault();
+ // I sure hope this doesn't break anything but it's needed to keep the removeMenu from activating
+ e.stopPropagation();
+ // nasty global event trap :-(
+ $(document).on('click', this.removeMenu);
+ this.$el.addClass('is-active');
+ },
+ selectGradeType : function(e) {
+ e.preventDefault();
+
+ this.removeMenu(e);
+
+ // TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
+ // of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
+ this.assignmentGrade.save('graderType', $(e.target).text());
+
+ this.render();
+ }
+})
\ No newline at end of file
diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js
new file mode 100644
index 0000000000..31b8586d5a
--- /dev/null
+++ b/cms/static/js/views/settings/main_settings_view.js
@@ -0,0 +1,673 @@
+if (!CMS.Views['Settings']) CMS.Views.Settings = {};
+
+// TODO move to common place
+CMS.Views.ValidatingView = Backbone.View.extend({
+ // Intended as an abstract class which catches validation errors on the model and
+ // decorates the fields. Needs wiring per class, but this initialization shows how
+ // either have your init call this one or copy the contents
+ initialize : function() {
+ this.model.on('error', this.handleValidationError, this);
+ this.selectorToField = _.invert(this.fieldToSelectorMap);
+ },
+
+ errorTemplate : _.template('<%= message %>'),
+
+ events : {
+ "blur input" : "clearValidationErrors",
+ "blur textarea" : "clearValidationErrors"
+ },
+ fieldToSelectorMap : {
+ // Your subclass must populate this w/ all of the model keys and dom selectors
+ // which may be the subjects of validation errors
+ },
+ _cacheValidationErrors : [],
+ handleValidationError : function(model, error) {
+ // error is object w/ fields and error strings
+ for (var field in error) {
+ var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
+ this._cacheValidationErrors.push(ele);
+ if ($(ele).is('div')) {
+ // put error on the contained inputs
+ $(ele).find('input, textarea').addClass('error');
+ }
+ else $(ele).addClass('error');
+ $(ele).parent().append(this.errorTemplate({message : error[field]}));
+ }
+ },
+
+ clearValidationErrors : function() {
+ // error is object w/ fields and error strings
+ while (this._cacheValidationErrors.length > 0) {
+ var ele = this._cacheValidationErrors.pop();
+ if ($(ele).is('div')) {
+ // put error on the contained inputs
+ $(ele).find('input, textarea').removeClass('error');
+ }
+ else $(ele).removeClass('error');
+ $(ele).nextAll('.message-error').remove();
+ }
+ },
+
+ saveIfChanged : function(event) {
+ // returns true if the value changed and was thus sent to server
+ var field = this.selectorToField[event.currentTarget.id];
+ var currentVal = this.model.get(field);
+ var newVal = $(event.currentTarget).val();
+ if (currentVal != newVal) {
+ this.clearValidationErrors();
+ this.model.save(field, newVal);
+ 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();
+ }
+
+});
+
+CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
+ // Model class is CMS.Models.Settings.CourseDetails
+ events : {
+ "blur input" : "updateModel",
+ "blur textarea" : "updateModel",
+ 'click .remove-course-syllabus' : "removeSyllabus",
+ 'click .new-course-syllabus' : 'assetSyllabus',
+ 'click .remove-course-introduction-video' : "removeVideo",
+ 'focus #course-overview' : "codeMirrorize"
+ },
+ initialize : function() {
+ // TODO move the html frag to a loaded asset
+ this.fileAnchorTemplate = _.template(' 📄<%= filename %>');
+ this.model.on('error', this.handleValidationError, this);
+ this.selectorToField = _.invert(this.fieldToSelectorMap);
+ },
+
+ render: function() {
+ this.setupDatePicker('start_date');
+ this.setupDatePicker('end_date');
+ this.setupDatePicker('enrollment_start');
+ this.setupDatePicker('enrollment_end');
+
+ if (this.model.has('syllabus')) {
+ this.$el.find(this.fieldToSelectorMap['syllabus']).html(
+ this.fileAnchorTemplate({
+ fullpath : this.model.get('syllabus'),
+ filename: 'syllabus'}));
+ this.$el.find('.remove-course-syllabus').show();
+ }
+ else {
+ this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html("");
+ this.$el.find('.remove-course-syllabus').hide();
+ }
+
+ this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview'));
+ this.codeMirrorize(null, $('#course-overview')[0]);
+
+ this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample());
+ if (this.model.has('intro_video')) {
+ this.$el.find('.remove-course-introduction-video').show();
+ this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video'));
+ }
+ else this.$el.find('.remove-course-introduction-video').hide();
+
+ this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
+
+ return this;
+ },
+ fieldToSelectorMap : {
+ 'start_date' : "course-start",
+ 'end_date' : 'course-end',
+ 'enrollment_start' : 'enrollment-start',
+ 'enrollment_end' : 'enrollment-end',
+ 'syllabus' : '.current-course-syllabus .doc-filename',
+ 'overview' : 'course-overview',
+ 'intro_video' : 'course-introduction-video',
+ 'effort' : "course-effort"
+ },
+
+ setupDatePicker : function(fieldName) {
+ var cacheModel = this.model;
+ var div = this.$el.find('#' + this.fieldToSelectorMap[fieldName]);
+ var datefield = $(div).find(".date");
+ var timefield = $(div).find(".time");
+ var cachethis = this;
+ var savefield = function() {
+ cachethis.clearValidationErrors();
+ var date = datefield.datepicker('getDate');
+ if (date) {
+ var time = timefield.timepicker("getSecondsFromMidnight");
+ if (!time) {
+ time = 0;
+ }
+ var newVal = new Date(date.getTime() + time * 1000);
+ if (cacheModel.get(fieldName) != newVal) cacheModel.save(fieldName, newVal);
+ }
+ };
+
+ // instrument as date and time pickers
+ timefield.timepicker();
+
+ // FIXME being called 2x on each change. Was trapping datepicker onSelect b4 but change to datepair broke that
+ datefield.datepicker({ onSelect : savefield });
+ timefield.on('changeTime', savefield);
+
+ 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;
+
+ 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});
+ },
+
+ 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) {
+ if (forcedTarget) {
+ thisTarget = forcedTarget;
+ thisTarget.id = $(thisTarget).attr('id');
+ } else {
+ thisTarget = e.currentTarget;
+ }
+
+ if (!this.codeMirrors[thisTarget.id]) {
+ var cachethis = this;
+ var field = this.selectorToField[thisTarget.id];
+ this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, {
+ mode: "text/html", lineNumbers: true, lineWrapping: true,
+ onBlur : function(mirror) {
+ mirror.save();
+ cachethis.clearValidationErrors();
+ var newVal = mirror.getValue();
+ if (cachethis.model.get(field) != newVal) cachethis.model.save(field, newVal);
+ }
+ });
+ }
+ }
+
+});
+
+CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
+ // Model class is CMS.Models.Settings.CourseGradingPolicy
+ events : {
+ "blur input" : "updateModel",
+ "blur textarea" : "updateModel",
+ "blur span[contenteditable=true]" : "updateDesignation",
+ "click .settings-extra header" : "showSettingsExtras",
+ "click .new-grade-button" : "addNewGrade",
+ "click .remove-button" : "removeGrade",
+ "click .add-grading-data" : "addAssignmentType"
+ },
+ initialize : function() {
+ // load template for grading view
+ var self = this;
+ this.gradeCutoffTemplate = _.template('' +
+ '<%= descriptor %>' +
+ '' +
+ '<% if (removable) {%>remove<% ;} %>' +
+ '');
+
+ // Instrument grading scale
+ // convert cutoffs to inversely ordered list
+ var modelCutoffs = this.model.get('grade_cutoffs');
+ for (var cutoff in modelCutoffs) {
+ this.descendingCutoffs.push({designation: cutoff, cutoff: Math.round(modelCutoffs[cutoff] * 100)});
+ }
+ this.descendingCutoffs = _.sortBy(this.descendingCutoffs,
+ function (gradeEle) { return -gradeEle['cutoff']; });
+
+ // Instrument grace period
+ this.$el.find('#course-grading-graceperiod').timepicker();
+
+ // instantiates an editor template for each update in the collection
+ // Because this calls render, put it after everything which render may depend upon to prevent race condition.
+ window.templateLoader.loadRemoteTemplate("course_grade_policy",
+ "/static/client_templates/course_grade_policy.html",
+ function (raw_template) {
+ self.template = _.template(raw_template);
+ self.render();
+ }
+ );
+ this.model.on('error', this.handleValidationError, this);
+ this.model.get('graders').on('remove', this.render, this);
+ this.model.get('graders').on('reset', this.render, this);
+ this.model.get('graders').on('add', this.render, this);
+ this.selectorToField = _.invert(this.fieldToSelectorMap);
+ },
+
+ render: function() {
+ // prevent bootstrap race condition by event dispatch
+ if (!this.template) return;
+
+ // Create and render the grading type subs
+ var self = this;
+ var gradelist = this.$el.find('.course-grading-assignment-list');
+ // Undo the double invocation error. At some point, fix the double invocation
+ $(gradelist).empty();
+ var gradeCollection = this.model.get('graders');
+ gradeCollection.each(function(gradeModel) {
+ $(gradelist).append(self.template({model : gradeModel }));
+ var newEle = gradelist.children().last();
+ var newView = new CMS.Views.Settings.GraderView({el: newEle,
+ model : gradeModel, collection : gradeCollection });
+ });
+
+ // render the grade cutoffs
+ this.renderCutoffBar();
+
+ var graceEle = this.$el.find('#course-grading-graceperiod');
+ graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime
+ if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate());
+
+ return this;
+ },
+ addAssignmentType : function(e) {
+ e.preventDefault();
+ this.model.get('graders').push({});
+ },
+ fieldToSelectorMap : {
+ 'grace_period' : 'course-grading-graceperiod'
+ },
+ updateModel : function(event) {
+ if (!this.selectorToField[event.currentTarget.id]) return;
+
+ switch (this.selectorToField[event.currentTarget.id]) {
+ case 'grace_period':
+ this.clearValidationErrors();
+ var newVal = this.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
+ if (this.model.get('grace_period') != newVal) this.model.save('grace_period', newVal);
+ 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;
+ },
+ {}));
+ },
+
+ 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();
+ e.preventDefault();
+ }
+
+});
\ No newline at end of file
diff --git a/cms/static/sass/_assets.scss b/cms/static/sass/_assets.scss
index 29c3d4000d..23d9ea4d9a 100644
--- a/cms/static/sass/_assets.scss
+++ b/cms/static/sass/_assets.scss
@@ -5,14 +5,6 @@
background-color: #fff;
}
- .upload-button {
- @include blue-button;
- float: left;
- margin-right: 20px;
- padding: 8px 30px 10px;
- font-size: 12px;
- }
-
.asset-library {
@include clearfix;
diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss
index 7f5dcacf8b..92cde28756 100644
--- a/cms/static/sass/_base.scss
+++ b/cms/static/sass/_base.scss
@@ -6,11 +6,15 @@
body {
min-width: 980px;
- background: #f3f4f5;
- font-family: 'Open Sans', sans-serif;
+ background: rgb(240, 241, 245);
font-size: 16px;
line-height: 1.6;
- color: #3c3c3c;
+ color: $baseFontColor;
+}
+
+body,
+input {
+ font-family: 'Open Sans', sans-serif;
}
a {
@@ -26,7 +30,8 @@ a {
h1 {
float: left;
font-size: 28px;
- margin: 36px 6px;
+ font-weight: 300;
+ margin: 24px 6px;
}
.waiting {
@@ -34,8 +39,7 @@ h1 {
}
.page-actions {
- float: right;
- margin-top: 42px;
+ margin-bottom: 30px;
}
.main-wrapper {
@@ -53,13 +57,6 @@ h1 {
}
}
-.window {
- background: #fff;
- border: 1px solid $darkGrey;
- border-radius: 3px;
- @include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
-}
-
.sidebar {
float: right;
width: 28%;
@@ -80,17 +77,18 @@ footer {
input[type="text"],
input[type="email"],
-input[type="password"] {
+input[type="password"],
+textarea.text {
padding: 6px 8px 8px;
@include box-sizing(border-box);
- border: 1px solid #b0b6c2;
+ border: 1px solid $mediumGrey;
border-radius: 2px;
- @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3));
- background-color: #edf1f5;
+ @include linear-gradient($lightGrey, tint($lightGrey, 90%));
+ background-color: $lightGrey;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
font-family: 'Open Sans', sans-serif;
font-size: 11px;
- color: #3c3c3c;
+ color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder,
@@ -98,6 +96,11 @@ input[type="password"] {
&:-ms-input-placeholder {
color: #979faf;
}
+
+ &:focus {
+ @include linear-gradient($paleYellow, tint($paleYellow, 90%));
+ outline: 0;
+ }
}
input.search {
@@ -107,7 +110,7 @@ input.search {
border-radius: 20px;
background: url(../img/search-icon.png) no-repeat 8px 7px #edf1f5;
font-family: 'Open Sans', sans-serif;
- color: #3c3c3c;
+ color: $baseFontColor;
outline: 0;
&::-webkit-input-placeholder {
@@ -126,12 +129,18 @@ code {
font-family: Monaco, monospace;
}
+.CodeMirror {
+ font-size: 13px;
+ border: 1px solid $darkGrey;
+ background: #fff;
+}
+
.text-editor {
width: 100%;
min-height: 80px;
padding: 10px;
@include box-sizing(border-box);
- border: 1px solid #b0b6c2;
+ border: 1px solid $mediumGrey;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.3));
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
@@ -173,27 +182,32 @@ code {
padding: 10px 0;
}
+.details {
+ display: none;
+ margin-bottom: 30px;
+ font-size: 14px;
+}
+
.window {
margin-bottom: 20px;
+ border: 1px solid $mediumGrey;
+ border-radius: 3px;
+ background: #fff;
+ @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
.window-contents {
padding: 20px;
}
- .details {
- margin-bottom: 30px;
- font-size: 14px;
- }
-
h4 {
padding: 6px 14px;
- border-bottom: 1px solid #cbd1db;
- border-radius: 3px 3px 0 0;
- @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0) 70%);
- background-color: #edf1f5;
- @include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset);
+ border-bottom: 1px solid $mediumGrey;
+ border-radius: 2px 2px 0 0;
+ @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
+ background-color: $lightBluishGrey;
+ @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset);
font-size: 14px;
- font-weight: 600;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
}
label {
@@ -345,8 +359,9 @@ body.show-wip {
}
.new-button {
- @include grey-button;
- padding: 20px 0;
+ @include green-button;
+ font-size: 13px;
+ padding: 8px 20px 10px;
text-align: center;
&.big {
@@ -367,4 +382,39 @@ body.show-wip {
.delete-icon {
margin-right: 4px;
}
+}
+
+.delete-button.standard {
+
+ &:hover {
+ background-color: tint($orange, 75%);
+ }
+}
+
+.tooltip {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 99999;
+ padding: 0 10px;
+ border-radius: 3px;
+ background: rgba(0, 0, 0, 0.85);
+ font-size: 11px;
+ font-weight: normal;
+ line-height: 26px;
+ color: #fff;
+ pointer-events: none;
+ opacity: 0;
+ @include transition(opacity 0.1s ease-out);
+
+ &:after {
+ content: 'â–ľ';
+ display: block;
+ position: absolute;
+ bottom: -14px;
+ left: 50%;
+ margin-left: -7px;
+ font-size: 20px;
+ color: rgba(0, 0, 0, 0.85);
+ }
}
\ No newline at end of file
diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss
index 80a82fb7f3..0f2e139455 100644
--- a/cms/static/sass/_cms_mixins.scss
+++ b/cms/static/sass/_cms_mixins.scss
@@ -47,18 +47,33 @@
}
}
+@mixin green-button {
+ @include button;
+ border: 1px solid #0d7011;
+ border-radius: 3px;
+ @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
+ background-color: $green;
+ color: #fff;
+
+ &:hover {
+ background-color: #129416;
+ color: #fff;
+ }
+}
+
@mixin white-button {
@include button;
- border: 1px solid $darkGrey;
+ border: 1px solid $mediumGrey;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
background-color: #dfe5eb;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
- color: #778192;
+ color: rgb(92, 103, 122);
+ text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
&:hover {
- background-color: #f2f6f9;
- color: #778192;
+ background-color: rgb(222, 236, 247);
+ color: rgb(92, 103, 122);
}
}
@@ -92,6 +107,28 @@
}
}
+@mixin green-button {
+ @include button;
+ border: 1px solid $darkGreen;
+ border-radius: 3px;
+ @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
+ background-color: $green;
+ @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
+ color: #fff;
+
+ &:hover {
+ background-color: $brightGreen;
+ color: #fff;
+ }
+
+ &.disabled {
+ border: 1px solid $disabledGreen !important;
+ background: $disabledGreen !important;
+ color: #fff !important;
+ @include box-shadow(none);
+ }
+}
+
@mixin dark-grey-button {
@include button;
border: 1px solid #1c1e20;
@@ -109,18 +146,17 @@
@mixin edit-box {
padding: 15px 20px;
border-radius: 3px;
- border: 1px solid #5597dd;
- @include linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0));
- background-color: #5597dd;
+ background-color: $lightBluishGrey2;
+ color: #3c3c3c;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset);
label {
- color: #fff;
+ color: $baseFontColor;
}
input,
textarea {
- border: 1px solid #3c3c3c;
+ border: 1px solid $darkGrey;
}
textarea {
@@ -140,21 +176,19 @@
}
.save-button {
- @include orange-button;
- border-color: #3c3c3c;
+ @include blue-button;
margin-top: 0;
}
.cancel-button {
@include white-button;
- border-color: #30649C;
margin-top: 0;
}
}
@mixin tree-view {
- border: 1px solid #ced2db;
- background: #edf1f5;
+ border: 1px solid $mediumGrey;
+ background: $lightGrey;
.branch {
margin-bottom: 10px;
@@ -200,15 +234,10 @@
content: "- draft";
}
- .public-item:after {
- content: "- public";
- }
-
.private-item:after {
content: "- private";
}
- .public-item,
.private-item {
color: #a4aab7;
}
@@ -219,7 +248,11 @@
}
a {
- color: #2c2e33;
+ color: $baseFontColor;
+
+ &.new-unit-item {
+ color: #6d788b;
+ }
}
ol {
@@ -242,3 +275,14 @@
}
}
}
+
+@mixin sr-text {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+}
\ No newline at end of file
diff --git a/cms/static/sass/_course-info.scss b/cms/static/sass/_course-info.scss
index 6eb08943b1..2ec22ebfea 100644
--- a/cms/static/sass/_course-info.scss
+++ b/cms/static/sass/_course-info.scss
@@ -1,77 +1,179 @@
-body.updates {
+.course-info {
h2 {
margin-bottom: 24px;
font-size: 22px;
font-weight: 300;
}
+
+ .course-info-wrapper {
+ display: table;
+ width: 100%;
+ clear: both;
+ }
+
+ .main-column,
+ .course-handouts {
+ float: none;
+ display: table-cell;
+ }
+
+ .main-column {
+ border-radius: 3px 0 0 3px;
+ border-right-color: $mediumGrey;
+ }
+
+ .CodeMirror {
+ border: 1px solid #3c3c3c;
+ background: #fff;
+ color: #3c3c3c;
+ }
}
.course-updates {
padding: 30px 40px;
+ margin: 0;
- li {
- padding: 24px 0 32px;
+ .update-list > li {
+ padding: 34px 0 42px;
border-top: 1px solid #cbd1db;
- }
- h3 {
- margin-bottom: 18px;
- font-size: 14px;
- font-weight: 700;
- color: #646464;
- letter-spacing: 1px;
- text-transform: uppercase;
+ &.editing {
+ position: relative;
+ z-index: 1001;
+ padding: 0;
+ border-top: none;
+ border-radius: 3px;
+ background: #fff;
+
+ .post-preview {
+ display: none;
+ }
+ }
+
+ h1 {
+ float: none;
+ font-size: 24px;
+ font-weight: 300;
+ }
+
+ h2 {
+ margin-bottom: 18px;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 30px;
+ color: #646464;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ }
+
+ h3 {
+ margin: 34px 0 11px;
+ font-size: 16px;
+ font-weight: 700;
+ }
}
.update-contents {
- padding-left: 30px;
-
p {
- font-size: 14px;
- line-height: 18px;
+ font-size: 16px;
+ line-height: 25px;
}
p + p {
- margin-top: 18px;
+ margin-top: 25px;
+ }
+
+ .primary {
+ border: 1px solid #ddd;
+ background: #f6f6f6;
+ padding: 20px;
}
}
.new-update-button {
- @include grey-button;
+ @include blue-button;
display: block;
text-align: center;
- padding: 12px 0;
+ padding: 18px 0;
margin-bottom: 28px;
}
.new-update-form {
@include edit-box;
margin-bottom: 24px;
+ padding: 30px;
+ border: none;
textarea {
height: 180px;
}
}
+
+ .post-actions {
+ float: right;
+
+ .edit-button,
+ .delete-button{
+ float: left;
+ @include white-button;
+ padding: 3px 10px 4px;
+ margin-left: 7px;
+ font-size: 12px;
+ font-weight: 400;
+
+ .edit-icon,
+ .delete-icon {
+ margin-right: 4px;
+ }
+ }
+ }
}
.course-handouts {
- padding: 15px 20px;
+ width: 30%;
+ padding: 20px 30px;
+ margin: 0;
+ border-radius: 0 3px 3px 0;
+ border-left: none;
+ background: $lightGrey;
- .new-handout-button {
- @include grey-button;
- display: block;
- text-align: center;
- padding: 12px 0;
- margin-bottom: 28px;
+ h2 {
+ font-size: 18px;
+ font-weight: 700;
}
- li {
- margin-bottom: 10px;
+ .edit-button {
+ float: right;
+ @include white-button;
+ padding: 3px 10px 4px;
+ margin-left: 7px;
+ font-size: 12px;
+ font-weight: 400;
+
+ .edit-icon,
+ .delete-icon {
+ margin-right: 4px;
+ }
+ }
+
+ .handouts-content {
font-size: 14px;
}
- .new-handout-form {
- @include edit-box;
- margin-bottom: 24px;
+ .treeview-handoutsnav li {
+ margin-bottom: 12px;
+ }
+}
+
+.edit-handouts-form {
+ @include edit-box;
+ position: absolute;
+ right: 0;
+ z-index: 10001;
+ width: 800px;
+ padding: 30px;
+
+ textarea {
+ height: 300px;
}
}
\ No newline at end of file
diff --git a/cms/static/sass/_courseware.scss b/cms/static/sass/_courseware.scss
index 23024f74e4..e2037916cb 100644
--- a/cms/static/sass/_courseware.scss
+++ b/cms/static/sass/_courseware.scss
@@ -1,21 +1,139 @@
+
input.courseware-unit-search-input {
float: left;
width: 260px;
background-color: #fff;
}
-.courseware-overview {
-
-}
+.branch {
+
+ .section-item {
+ @include clearfix();
+
+ .details {
+ display: block;
+ float: left;
+ margin-bottom: 0;
+ width: 650px;
+ }
+
+ .gradable-status {
+ float: right;
+ position: relative;
+ top: -4px;
+ right: 50px;
+ width: 145px;
+
+ .status-label {
+ position: absolute;
+ top: 2px;
+ right: -5px;
+ display: none;
+ width: 110px;
+ padding: 5px 40px 5px 10px;
+ @include border-radius(3px);
+ color: $lightGrey;
+ text-align: right;
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 16px;
+ }
+
+ .menu-toggle {
+ z-index: 10;
+ position: absolute;
+ top: 0;
+ right: 5px;
+ padding: 5px;
+ color: $mediumGrey;
+
+ &:hover, &.is-active {
+ color: $blue;
+ }
+ }
+
+ .menu {
+ z-index: 1;
+ display: none;
+ opacity: 0.0;
+ position: absolute;
+ top: -1px;
+ left: 5px;
+ margin: 0;
+ padding: 8px 12px;
+ background: $white;
+ border: 1px solid $mediumGrey;
+ font-size: 12px;
+ @include border-radius(4px);
+ @include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
+ @include transition(opacity .15s);
+
+
+ li {
+ width: 115px;
+ margin-bottom: 3px;
+ padding-bottom: 3px;
+ border-bottom: 1px solid $lightGrey;
+
+ &:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border: none;
+
+ a {
+ color: $darkGrey;
+ }
+ }
+ }
+
+ a {
+ color: $blue;
+
+ &.is-selected {
+ font-weight: bold;
+ }
+ }
+ }
+
+ // dropdown state
+ &.is-active {
+
+ .menu {
+ z-index: 1000;
+ display: block;
+ opacity: 1.0;
+ }
+
+ .menu-toggle {
+ z-index: 10000;
+ }
+ }
+
+ // set state
+ &.is-set {
+
+ .menu-toggle {
+ color: $blue;
+ }
+
+ .status-label {
+ display: block;
+ color: $blue;
+ }
+ }
+ }
+ }
+ }
+
.courseware-section {
position: relative;
background: #fff;
- border: 1px solid $darkGrey;
border-radius: 3px;
- margin: 10px 0;
+ border: 1px solid $mediumGrey;
+ margin-top: 15px;
padding-bottom: 12px;
- @include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
+ @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
&:first-child {
margin-top: 0;
@@ -49,7 +167,7 @@ input.courseware-unit-search-input {
margin-right: 15px;
strong {
- font-weight: 700;
+ font-weight: bold;
}
}
@@ -76,7 +194,7 @@ input.courseware-unit-search-input {
background: none;
@include box-shadow(none);
font-size: 13px;
- font-weight: 700;
+ font-weight: bold;
color: $blue;
cursor: pointer;
}
@@ -96,12 +214,199 @@ input.courseware-unit-search-input {
}
header {
- height: 75px;
+ min-height: 75px;
+ @include clearfix();
+
+ .item-details, .section-published-date {
+
+ }
.item-details {
- float: left;
- padding: 21px 0 0;
- }
+ display: inline-block;
+ padding: 20px 0 10px 0;
+ @include clearfix();
+
+ .section-name {
+ float: left;
+ margin-right: 10px;
+ width: 350px;
+ font-size: 19px;
+ font-weight: bold;
+ color: $blue;
+ }
+
+ .section-name-span {
+ cursor: pointer;
+ @include transition(color .15s);
+
+ &:hover {
+ color: $orange;
+ }
+ }
+
+ .section-name-edit {
+ position: relative;
+ width: 400px;
+ background: $white;
+
+ input {
+ font-size: 16px;
+ }
+
+ .save-button {
+ @include blue-button;
+ padding: 7px 20px 7px;
+ margin-right: 5px;
+ }
+
+ .cancel-button {
+ @include white-button;
+ padding: 7px 20px 7px;
+ }
+ }
+
+ .section-published-date {
+ float: right;
+ width: 265px;
+ margin-right: 220px;
+ @include border-radius(3px);
+ background: $lightGrey;
+
+ .published-status {
+ font-size: 12px;
+ margin-right: 15px;
+
+ strong {
+ font-weight: bold;
+ }
+ }
+
+ .schedule-button {
+ @include blue-button;
+ }
+
+ .edit-button {
+ @include blue-button;
+ }
+
+ .schedule-button,
+ .edit-button {
+ font-size: 11px;
+ padding: 3px 15px 5px;
+
+ }
+ }
+
+ .gradable-status {
+ position: absolute;
+ top: 20px;
+ right: 70px;
+ width: 145px;
+
+ .status-label {
+ position: absolute;
+ top: 0;
+ right: 2px;
+ display: none;
+ width: 100px;
+ padding: 10px 35px 10px 10px;
+ @include border-radius(3px);
+ background: $lightGrey;
+ color: $lightGrey;
+ text-align: right;
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 16px;
+ }
+
+ .menu-toggle {
+ z-index: 10;
+ position: absolute;
+ top: 2px;
+ right: 5px;
+ padding: 5px;
+ color: $lightGrey;
+
+ &:hover, &.is-active {
+ color: $blue;
+ }
+ }
+
+ .menu {
+ z-index: 1;
+ display: none;
+ opacity: 0.0;
+ position: absolute;
+ top: -1px;
+ left: 2px;
+ margin: 0;
+ padding: 8px 12px;
+ background: $white;
+ border: 1px solid $mediumGrey;
+ font-size: 12px;
+ @include border-radius(4px);
+ @include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
+ @include transition(opacity .15s);
+ @include transition(display .15s);
+
+
+ li {
+ width: 115px;
+ margin-bottom: 3px;
+ padding-bottom: 3px;
+ border-bottom: 1px solid $lightGrey;
+
+ &:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border: none;
+
+ a {
+ color: $darkGrey;
+ }
+ }
+ }
+
+ a {
+
+ &.is-selected {
+ font-weight: bold;
+ }
+ }
+ }
+
+ // dropdown state
+ &.is-active {
+
+ .menu {
+ z-index: 1000;
+ display: block;
+ opacity: 1.0;
+ }
+
+
+ .menu-toggle {
+ z-index: 10000;
+ }
+ }
+
+ // set state
+ &.is-set {
+
+ .menu-toggle {
+ color: $blue;
+ }
+
+ .status-label {
+ display: block;
+ color: $blue;
+ }
+ }
+
+ float: left;
+ padding: 21px 0 0;
+ }
+ }
.item-actions {
margin-top: 21px;
@@ -139,6 +444,10 @@ input.courseware-unit-search-input {
}
}
+ .section-name-form {
+ margin-bottom: 15px;
+ }
+
.section-name-edit {
input {
font-size: 16px;
@@ -146,13 +455,13 @@ input.courseware-unit-search-input {
.save-button {
@include blue-button;
- padding: 10px 20px;
+ padding: 7px 20px 7px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
- padding: 10px 20px;
+ padding: 7px 20px 7px;
}
}
@@ -161,7 +470,7 @@ input.courseware-unit-search-input {
color: #878e9d;
strong {
- font-weight: 700;
+ font-weight: bold;
}
}
@@ -183,7 +492,7 @@ input.courseware-unit-search-input {
&.new-section {
header {
height: auto;
- @include clearfix;
+ @include clearfix();
}
.expand-collapse-icon {
@@ -192,6 +501,17 @@ input.courseware-unit-search-input {
}
}
+.collapse-all-button {
+ float: right;
+ margin-top: 10px;
+ font-size: 13px;
+ color: $darkGrey;
+
+ .collapse-all-icon {
+ margin-right: 6px;
+ }
+}
+
.new-section-name,
.new-subsection-name-input {
width: 515px;
@@ -200,7 +520,7 @@ input.courseware-unit-search-input {
.new-section-name-save,
.new-subsection-name-save {
@include blue-button;
- padding: 6px 20px 8px;
+ padding: 4px 20px 7px;
margin: 0 5px;
color: #fff !important;
}
@@ -208,7 +528,7 @@ input.courseware-unit-search-input {
.new-section-name-cancel,
.new-subsection-name-cancel {
@include white-button;
- padding: 6px 20px 8px;
+ padding: 4px 20px 7px;
color: #8891a1 !important;
}
@@ -291,4 +611,11 @@ input.courseware-unit-search-input {
.cancel-button {
font-size: 16px;
}
+}
+
+.collapse-all-button {
+ float: right;
+ margin-top: 10px;
+ font-size: 13px;
+ color: $darkGrey;
}
\ No newline at end of file
diff --git a/cms/static/sass/_dashboard.scss b/cms/static/sass/_dashboard.scss
index 8821f3736c..0a9e992650 100644
--- a/cms/static/sass/_dashboard.scss
+++ b/cms/static/sass/_dashboard.scss
@@ -36,13 +36,6 @@
}
}
-.new-course-button {
- @include grey-button;
- display: block;
- padding: 20px;
- text-align: center;
-}
-
.new-course {
padding: 15px 25px;
margin-top: 20px;
@@ -89,7 +82,6 @@
.new-course-save {
@include blue-button;
- // padding: ;
}
.new-course-cancel {
diff --git a/cms/static/sass/_export.scss b/cms/static/sass/_export.scss
new file mode 100644
index 0000000000..e1ab7eb605
--- /dev/null
+++ b/cms/static/sass/_export.scss
@@ -0,0 +1,123 @@
+.export {
+ .export-overview {
+ @extend .window;
+ @include clearfix;
+ padding: 30px 40px;
+ }
+
+ .description {
+ float: left;
+ width: 62%;
+ margin-right: 3%;
+ font-size: 14px;
+
+ h2 {
+ font-weight: 700;
+ font-size: 19px;
+ margin-bottom: 20px;
+ }
+
+ strong {
+ font-weight: 700;
+ }
+
+ p + p {
+ margin-top: 20px;
+ }
+
+ ul {
+ margin: 20px 0;
+ list-style: disc inside;
+
+ li {
+ margin: 0 0 5px 0;
+ }
+ }
+ }
+
+ .export-form-wrapper {
+
+ .export-form {
+ float: left;
+ width: 35%;
+ padding: 25px 30px 35px;
+ @include box-sizing(border-box);
+ border: 1px solid $mediumGrey;
+ border-radius: 3px;
+ background: $lightGrey;
+ text-align: center;
+
+ h2 {
+ margin-bottom: 30px;
+ font-size: 26px;
+ font-weight: 300;
+ }
+
+ .error-block {
+ display: none;
+ margin-bottom: 15px;
+ font-size: 13px;
+ }
+
+ .error-block {
+ color: $error-red;
+ }
+
+ .button-export {
+ @include green-button;
+ padding: 10px 50px 11px;
+ font-size: 17px;
+ }
+
+ .message-status {
+ margin-top: 10px;
+ font-size: 12px;
+ }
+
+ .progress-bar {
+ display: none;
+ width: 350px;
+ height: 30px;
+ margin: 30px auto 10px;
+ border: 1px solid $blue;
+
+ &.loaded {
+ border-color: #66b93d;
+
+ .progress-fill {
+ background: #66b93d;
+ }
+ }
+ }
+
+ .progress-fill {
+ width: 0%;
+ height: 30px;
+ background: $blue;
+ color: #fff;
+ line-height: 48px;
+ }
+ }
+
+ // downloading state
+ &.is-downloading {
+
+ .progress-bar {
+ display: block;
+ }
+
+ .button-export {
+ padding: 10px 50px 11px;
+ font-size: 17px;
+
+ &.disabled {
+
+ pointer-events: none;
+ cursor: default;
+ }
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/cms/static/sass/_graphics.scss b/cms/static/sass/_graphics.scss
index 4a63a8a885..4ed22c570d 100644
--- a/cms/static/sass/_graphics.scss
+++ b/cms/static/sass/_graphics.scss
@@ -34,6 +34,14 @@
background: url(../img/video-icon.png) no-repeat;
}
+.upload-icon {
+ display: inline-block;
+ width: 22px;
+ height: 13px;
+ margin-right: 5px;
+ background: url(../img/upload-icon.png) no-repeat;
+}
+
.list-icon {
display: inline-block;
width: 14px;
@@ -56,6 +64,27 @@
background: url(../img/home-icon.png) no-repeat;
}
+.small-home-icon {
+ display: inline-block;
+ width: 16px;
+ height: 14px;
+ background: url(../img/small-home-icon.png) no-repeat;
+}
+
+.log-out-icon {
+ display: inline-block;
+ width: 15px;
+ height: 13px;
+ background: url(../img/log-out-icon.png) no-repeat;
+}
+
+.collapse-all-icon {
+ display: inline-block;
+ width: 15px;
+ height: 9px;
+ background: url(../img/collapse-all-icon.png) no-repeat;
+}
+
.calendar-icon {
display: inline-block;
width: 12px;
diff --git a/cms/static/sass/_header.scss b/cms/static/sass/_header.scss
index e80c7aa3b1..e031e16f50 100644
--- a/cms/static/sass/_header.scss
+++ b/cms/static/sass/_header.scss
@@ -5,18 +5,15 @@ body.no-header {
}
@mixin active {
- @include linear-gradient(top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.3));
- @include box-shadow(0 2px 8px rgba(0, 0, 0, .7) inset);
+ @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
+ background-color: rgba(255, 255, 255, .3);
+ @include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
.primary-header {
width: 100%;
- height: 36px;
- border-bottom: 1px solid #2c2e33;
- @include linear-gradient(top, #686b76, #54565e);
- font-size: 13px;
- color: #fff;
- @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset);
+ margin-bottom: 30px;
&.active-tab-courseware #courseware-tab {
@include active;
@@ -34,23 +31,16 @@ body.no-header {
@include active;
}
+ &.active-tab-settings #settings-tab {
+ @include active;
+ }
+
&.active-tab-import #import-tab {
@include active;
}
- #import-tab {
- @include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44);
- }
-
- .left {
- width: 750px;
- }
-
- .class-name {
- max-width: 350px;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
+ &.active-tab-export #export-tab {
+ @include active;
}
.drop-icon {
@@ -63,26 +53,57 @@ body.no-header {
line-height: 18px;
}
- a,
- .username {
- float: left;
- display: inline-block;
- height: 29px;
- padding: 7px 15px 0;
- color: #e4e6ee;
+ .class-nav-bar {
+ clear: both;
+ @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
+ background-color: $lightBluishGrey;
+ @include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
}
- .class-nav,
- .class-nav li {
- float: left;
- }
+ .class-nav {
+ @include clearfix;
- a {
- @include box-shadow(1px 0 0 #787981 inset, -1px 0 0 #3d3e44 inset, 1px 0 0 #787981, -1px 0 0 #3d3e44);
+ a {
+ float: left;
+ display: inline-block;
+ padding: 15px 25px 17px;
+ font-size: 15px;
+ color: #3c3c3c;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
- &:hover {
- background: rgba(255, 255, 255, .1);
+ &:hover {
+ @include linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0));
+ }
}
+ li {
+ float: left;
+ }
+ }
+
+ .class {
+ @include clearfix;
+ height: 100%;
+ font-size: 12px;
+ color: rgb(163, 171, 184);
+ @include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .1));
+ background-color: rgb(47, 53, 63);
+
+ a {
+ display: inline-block;
+ height: 20px;
+ padding: 5px 10px 6px;
+ color: rgb(163, 171, 184);
+ }
+
+ .home {
+ position: relative;
+ top: 1px;
+ }
+
+ .log-out {
+ position: relative;
+ top: 3px;
+ }
}
}
\ No newline at end of file
diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss
new file mode 100644
index 0000000000..b11378145b
--- /dev/null
+++ b/cms/static/sass/_settings.scss
@@ -0,0 +1,800 @@
+.settings {
+ .settings-overview {
+ @extend .window;
+ @include clearfix;
+ display: table;
+ width: 100%;
+
+ // layout
+ .sidebar {
+ display: table-cell;
+ float: none;
+ width: 20%;
+ padding: 30px 0 30px 20px;
+ @include border-radius(3px 0 0 3px);
+ background: $lightGrey;
+ }
+
+ .main-column {
+ display: table-cell;
+ float: none;
+ width: 80%;
+ padding: 30px 40px 30px 60px;
+ }
+
+ .settings-page-menu {
+ a {
+ display: block;
+ padding-left: 20px;
+ line-height: 52px;
+
+ &.is-shown {
+ background: #fff;
+ @include border-radius(5px 0 0 5px);
+ }
+ }
+ }
+
+ .settings-page-section {
+ > .alert {
+ display: none;
+
+ &.is-shown {
+ display: block;
+ }
+ }
+
+ > section {
+ display: none;
+ margin-bottom: 40px;
+
+ &.is-shown {
+ display: block;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ > .title {
+ margin-bottom: 30px;
+ font-size: 28px;
+ font-weight: 300;
+ color: $blue;
+ }
+
+ > section {
+ margin-bottom: 100px;
+ @include clearfix;
+
+ header {
+ @include clearfix;
+ border-bottom: 1px solid $mediumGrey;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+
+ h3 {
+ color: $darkGrey;
+ float: left;
+
+ margin: 0 40px 0 0;
+ text-transform: uppercase;
+ }
+
+ .detail {
+ float: right;
+ margin-top: 3px;
+ color: $mediumGrey;
+ font-size: 13px;
+ }
+ }
+
+ &:last-child {
+ padding-bottom: 0;
+ border-bottom: none;
+ }
+ }
+ }
+ }
+
+ // form basics
+ label, .label {
+ padding: 0;
+ border: none;
+ background: none;
+ font-size: 15px;
+ font-weight: 400;
+
+ &.check-label {
+ display: inline;
+ margin-left: 10px;
+ }
+
+ &.ranges {
+ margin-bottom: 20px;
+ }
+ }
+
+ input, textarea {
+ @include transition(all 1s ease-in-out);
+ @include box-sizing(border-box);
+ font-size: 15px;
+
+ &.long {
+ width: 100%;
+ min-width: 400px;
+ }
+
+ &.tall {
+ height: 200px;
+ }
+
+ &.short {
+ min-width: 100px;
+ width: 25%;
+ }
+
+ &.date {
+ display: block !important;
+ }
+
+ &.time {
+ width: 85px !important;
+ min-width: 85px !important;
+ }
+
+ &:disabled {
+ border: none;
+ @include box-shadow(none);
+ padding: 0;
+ color: $darkGrey !important;
+ font-weight: bold;
+ background: #fff;
+ }
+ }
+
+ textarea.tinymce {
+ border: 1px solid $darkGrey;
+ height: 300px;
+ }
+
+ input[type="checkbox"], input[type="radio"] {
+
+ }
+
+ input:disabled + .copy > label, input:disabled + .label {
+ color: $mediumGrey;
+ }
+
+
+ .input-default input, .input-default textarea {
+ color: $mediumGrey;
+ background: $lightGrey;
+ }
+
+ ::-webkit-input-placeholder {
+ color: $mediumGrey;
+ font-size: 13px;
+ }
+ :-moz-placeholder {
+ color: $mediumGrey;
+ font-size: 13px;
+ }
+
+ .tip {
+ color: $mediumGrey;
+ font-size: 13px;
+ }
+
+
+ // form layouts
+ .row {
+ margin-bottom: 30px;
+ padding-bottom: 30px;
+ border-bottom: 1px solid $lightGrey;
+
+ &:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+ }
+
+ // structural labels, not semantic labels per se
+ > label, .label {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ // tips
+ .tip-inline {
+ display: inline-block;
+ margin-left: 10px;
+ }
+
+ .tip-stacked {
+ display: block;
+ margin-top: 10px;
+ }
+
+ // structural field, not semantic fields per se
+ .field {
+ display: inline-block;
+ width: 100%;
+
+ > input, > textarea, .input {
+ display: inline-block;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .group {
+ input, textarea {
+ margin-bottom: 5px;
+ }
+
+ .label, label {
+ font-size: 13px;
+ }
+ }
+
+ // multi-field
+ &.multi {
+ display: block;
+ background: tint($lightGrey, 50%);
+ padding: 20px;
+ @include border-radius(4px);
+ @include box-sizing(border-box);
+
+ .group {
+ margin-bottom: 10px;
+ max-width: 175px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ input, .input, textarea {
+
+ }
+
+ .tip-stacked {
+ margin-top: 0;
+ }
+ }
+ }
+
+ // multi stacked
+ &.multi-stacked {
+
+ .group {
+ input, .input, textarea {
+ min-width: 370px;
+ width: 370px;
+ }
+ }
+ }
+
+ // multi-field inline
+ &.multi-inline {
+ @include clearfix;
+
+ .group {
+ float: left;
+ margin-right: 20px;
+
+ &:nth-child(2) {
+ margin-right: 0;
+ }
+
+ .input, input, textarea {
+ width: 100%;
+ }
+ }
+ }
+ }
+
+ // input-list
+ .input-list {
+
+ .input {
+ margin-bottom: 15px;
+ padding-bottom: 15px;
+ border-bottom: 1px dotted $lightGrey;
+ @include clearfix();
+
+ &:last-child {
+ border: 0;
+ }
+
+ .row {
+ }
+ }
+ }
+
+ //radio buttons and checkboxes
+ .input-radio {
+ @include clearfix();
+
+ input {
+ display: block;
+ float: left;
+ margin-right: 10px;
+ }
+
+ .copy {
+ position: relative;
+ top: -5px;
+ float: left;
+ width: 350px;
+ }
+
+ label {
+ display: block;
+ margin-bottom: 0;
+ }
+
+ .tip {
+ display: block;
+ margin-top: 0;
+ }
+
+ .message-error {
+
+ }
+ }
+
+ .input-checkbox {
+
+ }
+
+ // enumerated inputs
+ &.enum {
+ }
+ }
+
+ // layout - aligned label/field pairs
+ &.row-col2 {
+
+ > label, .label {
+ width: 200px;
+ }
+
+ .field {
+ width: 400px ! important;
+ }
+
+ &.multi-inline {
+ @include clearfix();
+
+ .group {
+ width: 170px;
+ }
+ }
+ }
+
+ .field-additional {
+ margin-left: 204px;
+ }
+ }
+
+ // editing controls - adding
+ .new-item, .replace-item {
+ clear: both;
+ display: block;
+ margin-top: 10px;
+ padding-bottom: 10px;
+ @include grey-button;
+ @include box-sizing(border-box);
+ }
+
+
+ // editing controls - removing
+ .delete-button {
+ float: right;
+ }
+
+ // editing controls - preview
+ .input-existing {
+ display: block !important;
+
+ .current {
+ width: 100%;
+ margin: 10px 0;
+ padding: 10px;
+ @include box-sizing(border-box);
+ @include border-radius(5px);
+ font-size: 14px;
+ background: tint($lightGrey, 50%);
+ @include clearfix();
+
+ .doc-filename {
+ display: inline-block;
+ width: 220px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .remove-doc-data {
+ display: inline-block;
+ margin-top: 0;
+ width: 150px;
+ }
+ }
+ }
+
+ // specific sections
+ .settings-details {
+
+ }
+
+ .settings-faculty {
+
+ .settings-faculty-members {
+
+ > header {
+ display: none;
+ }
+
+ .field .multi {
+ display: block;
+ margin-bottom: 40px;
+ padding: 20px;
+ background: tint($lightGrey, 50%);
+ @include border-radius(4px);
+ @include box-sizing(border-box);
+ }
+
+ .course-faculty-list-item {
+
+ .row {
+
+ &:nth-child(4) {
+ padding-bottom: 0;
+ border-bottom: none;
+ }
+ }
+
+ .remove-faculty-photo {
+ display: inline-block;
+ }
+ }
+
+ #course-faculty-bio-input {
+ margin-bottom: 0;
+ }
+
+ .new-course-faculty-item {
+ }
+
+ .current-faculty-photo {
+ padding: 0;
+
+ img {
+ display: block;
+ @include box-shadow(0 1px 3px rgba(0,0,0,0.1));
+ padding: 10px;
+ border: 2px solid $mediumGrey;
+ background: #fff;
+ }
+ }
+ }
+ }
+
+ .settings-grading {
+
+ .setting-grading-assignment-types {
+
+ .row .field.enum {
+ width: 684px;
+ }
+ }
+
+ .course-grading-assignment-list-item {
+
+ }
+
+ .input-list {
+ .row {
+
+ .input {
+ &:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ }
+ }
+ }
+ }
+ }
+
+ .settings-handouts {
+
+ }
+
+ .settings-problems {
+
+ > section {
+
+ &.is-shown {
+ display: block;
+ }
+ }
+ }
+
+ .settings-discussions {
+
+ .course-discussions-categories-list-item {
+
+ label {
+ display: none;
+ }
+
+ .group {
+ display: inline-block;
+ }
+
+ .remove-item {
+ display: inline-block !important;
+ margin-left: 10px;
+ }
+ }
+
+
+ }
+
+ // states
+ label.is-focused {
+ color: $blue;
+ @include transition(color 1s ease-in-out);
+ }
+
+ // extras/abbreviations
+ // .settings-extras {
+
+ // > header {
+ // cursor: pointer;
+
+ // &.active {
+
+ // }
+ // }
+
+ // > div {
+ // display: none;
+ // @include transition(display 0.25s ease-in-out);
+
+ // &.is-shown {
+ // display: block;
+ // }
+ // }
+ // }
+
+ input.error, textarea.error {
+ border-color: $red;
+ }
+
+ .message-error {
+ display: block;
+ margin-top: 5px;
+ color: $red;
+ font-size: 13px;
+ }
+
+ // misc
+ .divide {
+ display: none;
+ }
+
+ i.ss-icon {
+ position: relative;
+ top: 1px;
+ margin-right: 5px;
+ }
+
+ .well {
+ padding: 20px;
+ background: $lightGrey;
+ border: 1px solid $mediumGrey;
+ @include border-radius(4px);
+ @include box-shadow(0 1px 1px rgba(0,0,0,0.05) inset)
+ }
+ }
+
+
+
+ h3 {
+ margin-bottom: 30px;
+ font-size: 15px;
+ font-weight: 700;
+ color: $blue;
+ }
+
+ .grade-controls {
+ @include clearfix;
+ width: 642px;
+ }
+
+ .new-grade-button {
+ position: relative;
+ float: left;
+ display: block;
+ width: 29px;
+ height: 29px;
+ margin: 10px 20px 0 0;
+ border-radius: 20px;
+ border: 1px solid $darkGrey;
+ @include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
+ background-color: #d1dae3;
+ @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
+ color: #6d788b;
+
+ .plus-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-left: -6px;
+ margin-top: -6px;
+ }
+ }
+
+ .grade-slider {
+ float: left;
+ width: 580px;
+ margin-bottom: 10px;
+
+ .grade-bar {
+ position: relative;
+ width: 100%;
+ height: 50px;
+ background: $lightGrey;
+
+ .increments {
+ position: relative;
+
+ li {
+ position: absolute;
+ top: 52px;
+ width: 30px;
+ margin-left: -15px;
+ font-size: 9px;
+ text-align: center;
+
+ &.increment-0 {
+ left: 0;
+ }
+
+ &.increment-10 {
+ left: 10%;
+ }
+
+ &.increment-20 {
+ left: 20%;
+ }
+
+ &.increment-30 {
+ left: 30%;
+ }
+
+ &.increment-40 {
+ left: 40%;
+ }
+
+ &.increment-50 {
+ left: 50%;
+ }
+
+ &.increment-60 {
+ left: 60%;
+ }
+
+ &.increment-70 {
+ left: 70%;
+ }
+
+ &.increment-80 {
+ left: 80%;
+ }
+
+ &.increment-90 {
+ left: 90%;
+ }
+
+ &.increment-100 {
+ left: 100%;
+ }
+ }
+ }
+
+ .grade-specific-bar {
+ height: 50px !important;
+ }
+
+ .grades {
+ position: relative;
+
+ li {
+ position: absolute;
+ top: 0;
+ height: 50px;
+ text-align: right;
+ @include border-radius(2px);
+
+ &:hover,
+ &.is-dragging {
+ .remove-button {
+ display: block;
+ }
+ }
+
+ &.is-dragging {
+
+
+ }
+
+ .remove-button {
+ display: none;
+ position: absolute;
+ top: -17px;
+ right: 1px;
+ height: 17px;
+ font-size: 10px;
+ }
+
+ &:nth-child(1) {
+ background: #4fe696;
+ }
+
+ &:nth-child(2) {
+ background: #ffdf7e;
+ }
+
+ &:nth-child(3) {
+ background: #ffb657;
+ }
+
+ &:nth-child(4) {
+ background: #ef54a1;
+ }
+
+ &:nth-child(5),
+ &.bar-fail {
+ background: #fb336c;
+ }
+
+ .letter-grade {
+ display: block;
+ margin: 10px 15px 0 0;
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 14px;
+ }
+
+ .range {
+ display: block;
+ margin-right: 15px;
+ font-size: 10px;
+ line-height: 12px;
+ }
+
+ .drag-bar {
+ position: absolute;
+ top: 0;
+ right: -1px;
+ height: 50px;
+ width: 2px;
+ background-color: #fff;
+ @include box-shadow(-1px 0 3px rgba(0,0,0,0.1));
+
+ cursor: ew-resize;
+ @include transition(none);
+
+ &:hover {
+ width: 6px;
+ right: -2px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/cms/static/sass/_static-pages.scss b/cms/static/sass/_static-pages.scss
index c8ffa0316c..1ab3413682 100644
--- a/cms/static/sass/_static-pages.scss
+++ b/cms/static/sass/_static-pages.scss
@@ -7,7 +7,20 @@
}
.unit-body {
- padding: 30px 40px;
+ padding: 0;
+
+ .details {
+ display: block !important;
+
+ h2 {
+ margin: 0 0 5px 0;
+ }
+ }
+ }
+
+ .component-editor {
+ border: none;
+ border-radius: 0;
}
.components > li {
@@ -36,7 +49,7 @@
}
.drag-handle {
- background: url(../img/drag-handles.png) center no-repeat $lightGrey;
+ background: url(../img/drag-handles.png) center no-repeat #fff;
}
}
@@ -46,10 +59,10 @@
z-index: 11;
width: 35px;
border: none;
- background: url(../img/drag-handles.png) center no-repeat $lightGrey;
+ background: url(../img/drag-handles.png) center no-repeat #fff;
&:hover {
- background: url(../img/drag-handles.png) center no-repeat $lightGrey;
+ background: url(../img/drag-handles.png) center no-repeat #fff;
}
}
@@ -60,16 +73,24 @@
}
.component.editing {
+ border-left: 1px solid $mediumGrey;
+ border-right: 1px solid $mediumGrey;
+
.xmodule_display {
display: none;
}
}
+ .new .xmodule_display {
+ background: $yellow;
+ }
+
.xmodule_display {
padding: 20px 20px 22px;
font-size: 24px;
font-weight: 300;
- background: $lightGrey;
+ background: #fff;
+ @include transition(background-color 3s);
}
.static-page-item {
diff --git a/cms/static/sass/_subsection.scss b/cms/static/sass/_subsection.scss
index 0d7d518ffd..32c983ee3a 100644
--- a/cms/static/sass/_subsection.scss
+++ b/cms/static/sass/_subsection.scss
@@ -137,8 +137,7 @@
a {
font-size: 11px;
- font-weight: 700;
- line-height: 31px;
+ font-weight: bold;
text-transform: uppercase;
}
@@ -180,3 +179,109 @@
}
}
}
+
+.gradable {
+
+ label {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ .gradable-status {
+ position: relative;
+ top: -4px;
+ display: inline-block;
+ margin-left: 10px;
+ width: 65%;
+
+ .status-label {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ color: $blue;
+ border: none;
+ font-size: 11px;
+ font-weight: bold;
+ text-transform: uppercase;
+ }
+
+ .menu-toggle {
+ z-index: 100;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 20px;
+ background: transparent;
+
+ &:hover, &.is-active {
+ color: $blue;
+ }
+ }
+
+ .menu {
+ z-index: 1;
+ position: absolute;
+ top: -12px;
+ left: -7px;
+ display: none;
+ width: 100%;
+ margin: 0;
+ padding: 8px 12px;
+ opacity: 0.0;
+ background: $white;
+ border: 1px solid $mediumGrey;
+ font-size: 12px;
+ @include border-radius(4px);
+ @include box-shadow(0 1px 2px rgba(0, 0, 0, .2));
+ @include transition(opacity .15s);
+
+
+ li {
+ margin-bottom: 3px;
+ padding-bottom: 3px;
+ border-bottom: 1px solid $lightGrey;
+
+ &:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border: none;
+ }
+ }
+
+ a {
+
+ &.is-selected {
+ font-weight: bold;
+ }
+ }
+ }
+
+ // dropdown state
+ &.is-active {
+
+ .menu {
+ z-index: 10000;
+ display: block;
+ opacity: 1.0;
+ }
+
+ .menu-toggle {
+ z-index: 1000;
+ }
+ }
+
+ // set state
+ &.is-set {
+
+ .menu-toggle {
+ color: $blue;
+ }
+
+ .status-label {
+ display: block;
+ color: $blue;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss
index a7a33f6024..255738d32f 100644
--- a/cms/static/sass/_unit.scss
+++ b/cms/static/sass/_unit.scss
@@ -4,6 +4,7 @@
}
.main-column {
+ clear: both;
float: left;
width: 70%;
}
@@ -41,7 +42,7 @@
}
h2 {
- margin: 30px 40px;
+ margin: 30px 40px 30px 0;
color: #646464;
font-size: 19px;
font-weight: 300;
@@ -56,14 +57,10 @@
margin: 20px 40px;
&.new-component-item {
- padding: 0;
+ padding: 20px;
border: none;
- border-radius: 0;
-
- &.adding {
- background-color: $blue;
- border-color: #437fbf;
- }
+ border-radius: 3px;
+ background: $lightGrey;
.new-component-button {
display: block;
@@ -84,12 +81,13 @@
border-radius: 3px 3px 0 0;
}
- .new-component-type, .new-component-template {
- @include clearfix;
+ .new-component-type {
+ a,
+ li {
+ display: inline-block;
+ }
a {
- position: relative;
- float: left;
width: 100px;
height: 100px;
margin-right: 10px;
@@ -97,14 +95,8 @@
border-radius: 8px;
font-size: 13px;
line-height: 14px;
- color: #fff;
text-align: center;
- @include box-shadow(0 1px 1px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .4) inset);
- @include transition(background-color .15s);
-
- &:hover {
- background-color: rgba(255, 255, 255, .2);
- }
+ @include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
.name {
position: absolute;
@@ -117,23 +109,62 @@
}
}
+ .new-component-type,
.new-component-template {
+ @include clearfix;
+
a {
- height: 60px;
+ position: relative;
+ border: 1px solid $darkGreen;
+ background: $green;
+ color: #fff;
+ @include transition(background-color .15s);
+
+ &:hover {
+ background: $brightGreen;
+ }
}
}
- .new-component,
+ .new-component-template {
+ margin-bottom: 20px;
+
+ li:first-child {
+ a {
+ border-radius: 3px 3px 0 0;
+ }
+ }
+
+ li:last-child {
+ a {
+ border-radius: 0 0 3px 3px;
+ }
+ }
+
+ a {
+ display: block;
+ padding: 7px 20px;
+ border-bottom: none;
+ font-weight: 300;
+ }
+ }
+
+ .new-component {
+ text-align: center;
+
+ h5 {
+ color: $green;
+ }
+
+ }
+
.new-component-templates {
display: none;
- position: absolute;
- width: 100%;
padding: 20px;
@include clearfix;
.cancel-button {
- @include blue-button;
- border-color: #30649c;
+ @include white-button;
}
}
}
@@ -141,7 +172,7 @@
}
.component {
- border: 1px solid #d1ddec;
+ border: 1px solid $lightBluishGrey2;
border-radius: 3px;
background: #fff;
@include transition(none);
@@ -156,7 +187,8 @@
}
&.editing {
- border-color: #6696d7;
+ border: 1px solid $lightBluishGrey2;
+ z-index: 9999;
.drag-handle,
.component-actions {
@@ -172,11 +204,6 @@
position: absolute;
top: 7px;
right: 9px;
- @include transition(opacity .15s);
-
- a {
- color: $darkGrey;
- }
}
.drag-handle {
@@ -184,19 +211,20 @@
display: block;
top: -1px;
right: -16px;
- z-index: -1;
+ z-index: 10;
width: 15px;
height: 100%;
border-radius: 0 3px 3px 0;
- border: 1px solid #d1ddec;
- background: url(../img/white-drag-handles.png) center no-repeat #d1ddec;
+ border: 1px solid $lightBluishGrey2;
+ background: url(../img/white-drag-handles.png) center no-repeat $lightBluishGrey2;
cursor: move;
- @include transition(all .15s);
+ @include transition(none);
}
}
.xmodule_display {
padding: 40px 20px 20px;
+ overflow-x: auto;
}
.component-editor {
@@ -204,9 +232,6 @@
display: none;
padding: 20px;
border-radius: 2px 2px 0 0;
- @include linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1));
- background-color: $blue;
- color: #fff;
@include box-shadow(none);
.metadata_edit {
@@ -223,12 +248,6 @@
}
}
- .CodeMirror {
- border: 1px solid #3c3c3c;
- background: #fff;
- color: #3c3c3c;
- }
-
h3 {
margin-bottom: 10px;
font-size: 18px;
@@ -445,3 +464,26 @@
display: none;
}
}
+
+// editing units from courseware
+body.unit {
+
+ .component {
+ padding-top: 30px;
+
+ .component-actions {
+ @include box-sizing(border-box);
+ position: absolute;
+ width: 100%;
+ padding: 15px;
+ top: 0;
+ left: 0;
+ border-bottom: 1px solid $lightBluishGrey2;
+ background: $lightGrey;
+ }
+
+ &.editing {
+ padding-top: 0;
+ }
+ }
+}
diff --git a/cms/static/sass/_users.scss b/cms/static/sass/_users.scss
index bca9b4f2fb..e107bdbb6d 100644
--- a/cms/static/sass/_users.scss
+++ b/cms/static/sass/_users.scss
@@ -1,31 +1,8 @@
.users {
- .user-overview {
- @extend .window;
- padding: 30px 40px;
- }
-
- .new-user-button {
- @include grey-button;
- margin: 5px 8px;
- padding: 3px 10px 4px 10px;
- font-size: 12px;
-
- .plus-icon {
- position: relative;
- top: 1px;
- }
- }
-
- .list-header {
- @include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
- background-color: #ced2db;
- border-radius: 3px 3px 0 0;
- }
-
.new-user-form {
display: none;
padding: 15px 20px;
- background: $mediumGrey;
+ background-color: $lightBluishGrey2;
#result {
display: none;
@@ -55,21 +32,22 @@
.add-button {
@include blue-button;
+ padding: 5px 20px 9px;
}
.cancel-button {
@include white-button;
+ padding: 5px 20px 9px;
}
}
.user-list {
border: 1px solid $mediumGrey;
- border-top: none;
- background: $lightGrey;
+ background: #fff;
li {
position: relative;
- padding: 10px 20px;
+ padding: 20px;
border-bottom: 1px solid $mediumGrey;
&:last-child {
@@ -81,12 +59,19 @@
}
.user-name {
- width: 30%;
- font-weight: 700;
+ margin-right: 10px;
+ font-size: 24px;
+ font-weight: 300;
+ }
+
+ .user-email {
+ font-size: 14px;
+ font-style: italic;
+ color: $mediumGrey;
}
.item-actions {
- top: 9px;
+ top: 24px;
}
}
}
diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss
index fec65e4e11..a783abeaeb 100644
--- a/cms/static/sass/_variables.scss
+++ b/cms/static/sass/_variables.scss
@@ -10,13 +10,28 @@ $fg-min-width: 810px;
$sans-serif: 'Open Sans', $verdana;
$body-line-height: golden-ratio(.875em, 1);
+$white: rgb(255,255,255);
+$black: rgb(0,0,0);
$pink: rgb(182,37,104);
$error-red: rgb(253, 87, 87);
+$baseFontColor: #3c3c3c;
+$offBlack: #3c3c3c;
+$black: rgb(0,0,0);
+$white: rgb(255,255,255);
$blue: #5597dd;
$orange: #edbd3c;
+$red: #b20610;
+$green: #108614;
$lightGrey: #edf1f5;
-$mediumGrey: #ced2db;
+$mediumGrey: #b0b6c2;
$darkGrey: #8891a1;
$extraDarkGrey: #3d4043;
-$paleYellow: #fffcf1;
\ No newline at end of file
+$paleYellow: #fffcf1;
+$yellow: rgb(255, 254, 223);
+$green: rgb(37, 184, 90);
+$brightGreen: rgb(22, 202, 87);
+$disabledGreen: rgb(124, 206, 153);
+$darkGreen: rgb(52, 133, 76);
+$lightBluishGrey: rgb(197, 207, 223);
+$lightBluishGrey2: rgb(213, 220, 228);
diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss
index 038db536b1..e3463477c1 100644
--- a/cms/static/sass/base-style.scss
+++ b/cms/static/sass/base-style.scss
@@ -18,13 +18,14 @@
@import "static-pages";
@import "users";
@import "import";
+@import "export";
+@import "settings";
@import "course-info";
@import "landing";
@import "graphics";
@import "modal";
@import "alerts";
@import "login";
-@import "lms";
@import 'jquery-ui-calendar';
@import 'content-types';
diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html
index 900ef3b697..ca205157ab 100644
--- a/cms/templates/asset_index.html
+++ b/cms/templates/asset_index.html
@@ -21,7 +21,7 @@