From 2238019539f0352512e68fbb1373de1bdd4acf83 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 27 Nov 2012 11:32:03 -0500 Subject: [PATCH] Details tab works except for file references --- cms/static/js/models/course_relative.js | 30 ++++---- .../js/models/settings/course_details.js | 40 +++++++++++ .../js/models/settings/course_detais.js | 25 ------- .../js/models/settings/course_settings.js | 1 + .../js/views/settings/main_settings_view.js | 64 ++++++++++------- cms/templates/settings.html | 16 +++-- .../models/settings/course_details.py | 68 ++++++++++++------- common/djangoapps/util/converters.py | 20 ++++++ common/lib/xmodule/xmodule/course_module.py | 28 ++++++-- common/lib/xmodule/xmodule/x_module.py | 8 ++- 10 files changed, 197 insertions(+), 103 deletions(-) create mode 100644 cms/static/js/models/settings/course_details.js delete mode 100644 cms/static/js/models/settings/course_detais.js create mode 100644 common/djangoapps/util/converters.py diff --git a/cms/static/js/models/course_relative.js b/cms/static/js/models/course_relative.js index 5c42f4ce80..c33339ff48 100644 --- a/cms/static/js/models/course_relative.js +++ b/cms/static/js/models/course_relative.js @@ -1,4 +1,4 @@ -CMS.Models.Location = Backbone.Models.extend({ +CMS.Models.Location = Backbone.Model.extend({ defaults: { tag: "", org: "", @@ -14,29 +14,29 @@ CMS.Models.Location = Backbone.Models.extend({ (overrides['category'] ? overrides['category'] : this.get('category')) + "/" + (overrides['name'] ? overrides['name'] : this.get('name')) + "/"; }, - _tagPattern = /[^:]+/g, - _fieldPattern = new RegExp('[^/]+','g'), + _tagPattern : /[^:]+/g, + _fieldPattern : new RegExp('[^/]+','g'), parse: function(payload) { - if (payload instanceof Array) { + if (_.isArray(payload)) { return { tag: payload[0], - name: payload[1], + org: payload[1], course: payload[2], category: payload[3], name: payload[4] } } - else if (payload instanceof String) { + else if (_.isString(payload)) { var foundTag = this._tagPattern.exec(payload); if (foundTag) { - this._fieldPattern.lastIndex = this._tagPattern.lastIndex; + this._fieldPattern.lastIndex = this._tagPattern.lastIndex + 1; // skip over the colon return { - tag: foundTag, - name: this._fieldPattern.exec(payload), - course: this._fieldPattern.exec(payload), - category: this._fieldPattern.exec(payload), - name: this._fieldPattern.exec(payload) + tag: foundTag[0], + org: this._fieldPattern.exec(payload)[0], + course: this._fieldPattern.exec(payload)[0], + category: this._fieldPattern.exec(payload)[0], + name: this._fieldPattern.exec(payload)[0] } } else return null; @@ -47,13 +47,13 @@ CMS.Models.Location = Backbone.Models.extend({ } }); -CMS.Models.CourseRelative = Backbone.Models.extend({ +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.Collections.extend({ - model : CourseRelative +CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({ + model : CMS.Models.CourseRelative }); \ 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..3ae4a86f90 --- /dev/null +++ b/cms/static/js/models/settings/course_details.js @@ -0,0 +1,40 @@ +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; + }, + + urlRoot: function() { + var location = this.get('location'); + return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details'; + } +}); diff --git a/cms/static/js/models/settings/course_detais.js b/cms/static/js/models/settings/course_detais.js deleted file mode 100644 index c725ed46a2..0000000000 --- a/cms/static/js/models/settings/course_detais.js +++ /dev/null @@ -1,25 +0,0 @@ -CMS.Models.Settings.CourseDetails = Backbone.Models.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['location']) { - attributes.location = new CMS.Models.Location(attributes.location); - }; - }, - - urlRoot: function() { - var location = this.get('location'); - return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details'; - } -}); diff --git a/cms/static/js/models/settings/course_settings.js b/cms/static/js/models/settings/course_settings.js index c6959a8693..de4468c00b 100644 --- a/cms/static/js/models/settings/course_settings.js +++ b/cms/static/js/models/settings/course_settings.js @@ -1,3 +1,4 @@ +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: { diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js index 5a862dd5ef..b4019e12ea 100644 --- a/cms/static/js/views/settings/main_settings_view.js +++ b/cms/static/js/views/settings/main_settings_view.js @@ -1,3 +1,5 @@ +if (!CMS.Views['Settings']) CMS.Views.Settings = new Object(); + CMS.Views.Settings.Main = Backbone.View.extend({ // Model class is CMS.Models.Settings.CourseSettings // allow navigation between the tabs @@ -6,7 +8,7 @@ CMS.Views.Settings.Main = Backbone.View.extend({ }, currentTab: null, - subviews: {}, # indexed by tab name + subviews: {}, // indexed by tab name initialize: function() { // load templates @@ -31,8 +33,7 @@ CMS.Views.Settings.Main = Backbone.View.extend({ this.subviews[this.currentTab].render(); }); } - } - else this.callRenderFunction(); + else this.subviews[this.currentTab].render(); return this; }, @@ -42,7 +43,7 @@ CMS.Views.Settings.Main = Backbone.View.extend({ case 'details': return new CMS.Views.Settings.Details({ el: this.$el.find('.settings-' + this.currentTab), - model: this.model.get(this.currentTab); + model: this.model.get(this.currentTab) }); break; case 'faculty': @@ -72,6 +73,7 @@ CMS.Views.Settings.Details = Backbone.View.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", @@ -80,15 +82,13 @@ CMS.Views.Settings.Details = Backbone.View.extend({ initialize : function() { // TODO move the html frag to a loaded asset this.fileAnchorTemplate = _.template(' 📄<%= filename %>'); - // Save every change as it occurs. This may be too noisy!!! If not every change, then need sophisticated logic. - this.model.on('change', this.model.save); }, render: function() { - if (this.model.has('start_date')) this.$el.find('#course-start-date').datepicker('setDate', this.model.get('start_date')); - if (this.model.has('end_date')) this.$el.find('#course-end-date').datepicker('setDate', this.model.get('end_date')); - if (this.model.has('enrollment_start')) this.$el.find('#course-enrollment-start').datepicker('setDate', this.model.get('enrollment_start')); - if (this.model.has('enrollment_end')) this.$el.find('#course-enrollment-end').datepicker('setDate', this.model.get('enrollment_end')); + this.setupDatePicker('#course-start-date', 'start_date'); + this.setupDatePicker('#course-end-date', 'end_date'); + this.setupDatePicker('#course-enrollment-start-date', 'enrollment_start'); + this.setupDatePicker('#course-enrollment-end-date', 'enrollment_end'); if (this.model.has('syllabus')) { this.$el.find('.current-course-syllabus .doc-filename').html( @@ -97,46 +97,58 @@ CMS.Views.Settings.Details = Backbone.View.extend({ filename: 'syllabus'})); this.$el.find('.remove-course-syllabus').show(); } - else this.$el.find('.remove-course-syllabus').hide(); + else { + this.$el.find('.current-course-syllabus .doc-filename').html(""); + this.$el.find('.remove-course-syllabus').hide(); + } - if (this.model.has('overview')) - this.$el.find('#course-overview').text(this.model.get('overview')); + this.$el.find('#course-overview').val(this.model.get('overview')); + this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.get('intro_video')); if (this.model.has('intro_video')) { - this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.get('intro_video')); this.$el.find('.remove-course-introduction-video').show(); } else this.$el.find('.remove-course-introduction-video').hide(); + + this.$el.find("#course-effort").val(this.model.get('effort')); + + return this; + }, + + setupDatePicker : function(elementName, fieldName) { + var cacheModel = this.model; + var picker = this.$el.find(elementName); + picker.datepicker({ onSelect : function(newVal) { + cacheModel.save(fieldName, new Date(newVal)); + }}); + picker.datepicker('setDate', this.model.get(fieldName)); }, updateModel: function(event) { // figure out which field switch (event.currentTarget.id) { - case 'course-start-date': - var val = $(event.currentTarget).datepicker('getDate'); - this.model.set('start_date', val); - break; + case 'course-start-date': // handled via onSelect method case 'course-end-date': - this.model.set('end_date', $(event.currentTarget).datepicker('getDate')); - break; case 'course-enrollment-start-date': - this.model.set('enrollment_start', $(event.currentTarget).datepicker('getDate')); - break; case 'course-enrollment-end-date': - this.model.set('enrollment_end', $(event.currentTarget).datepicker('getDate')); break; case 'course-overview': - this.model.set('overview', $(event.currentTarget).text()); + this.model.save('overview', $(event.currentTarget).val()); break; + case 'course-effort': + this.model.save('effort', $(event.currentTarget).val()); + break; + default: break; } + }, removeSyllabus: function() { - if (this.model.has('syllabus')) this.model.set({'syllabus': null}); + if (this.model.has('syllabus')) this.model.save({'syllabus': null}); }, assetSyllabus : function() { @@ -144,7 +156,7 @@ CMS.Views.Settings.Details = Backbone.View.extend({ }, removeVideo: function() { - if (this.model.has('intro_video')) this.model.set({'intro_video': null}); + if (this.model.has('intro_video')) this.model.save({'intro_video': null}); }, assetVideo : function() { diff --git a/cms/templates/settings.html b/cms/templates/settings.html index fc58674847..9d5514c90d 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -3,6 +3,10 @@ <%block name="title">Settings <%namespace name='static' file='static_content.html'/> +<%! +from contentstore import utils +%> + <%block name="jsextra"> @@ -24,9 +28,9 @@ details: new CMS.Models.Settings.CourseDetails(${course_details|n},{parse:true}) }); - var editor = new CMS.Views.CourseInfoEdit({ + var editor = new CMS.Views.Settings.Main({ el: $('.main-wrapper'), - model : settingsModel) + model : settingsModel }); $('.set-date').datepicker({ 'dateFormat': 'm/d/yy' }); @@ -147,7 +151,7 @@
- This is used in your course URL, and cannot be changed + This is used in your course URL, and cannot be changed
@@ -157,7 +161,7 @@
- This is used in your course URL, and cannot be changed + This is used in your course URL, and cannot be changed
@@ -168,7 +172,7 @@
e.g. 101x - This is used in your course URL, and cannot be changed + This is used in your course URL, and cannot be changed
@@ -256,7 +260,7 @@
- Introductions, prerequisites, FAQs that are used on your course summary page + Introductions, prerequisites, FAQs that are used on your course summary page
diff --git a/common/djangoapps/models/settings/course_details.py b/common/djangoapps/models/settings/course_details.py index ed28733283..9dcdea4c16 100644 --- a/common/djangoapps/models/settings/course_details.py +++ b/common/djangoapps/models/settings/course_details.py @@ -4,6 +4,8 @@ from xmodule.modulestore import Location from xmodule.modulestore.exceptions import ItemNotFoundError import json from json.encoder import JSONEncoder +import time +from util.converters import time_to_date, jsdate_to_time class CourseDetails: def __init__(self, location): @@ -29,11 +31,6 @@ class CourseDetails: descriptor = modulestore('direct').get_item(course_location) - ## DEBUG verify that this is a ClassDescriptor object - if not isinstance(descriptor, CourseDescriptor): - print("oops, not the expected type: ", descriptor) - - ## FIXME convert these from time.struct_time objects to something the client wants course.start_date = descriptor.start course.end_date = descriptor.end course.enrollment_start = descriptor.enrollment_start @@ -45,19 +42,19 @@ class CourseDetails: except ItemNotFoundError: pass - temploc = course_location._replace(name='overview') + temploc = temploc._replace(name='overview') try: course.overview = modulestore('direct').get_item(temploc).definition['data'] except ItemNotFoundError: pass - temploc = course_location._replace(name='effort') + temploc = temploc._replace(name='effort') try: course.effort = modulestore('direct').get_item(temploc).definition['data'] except ItemNotFoundError: pass - temploc = course_location._replace(name='video') + temploc = temploc._replace(name='video') try: course.intro_video = modulestore('direct').get_item(temploc).definition['data'] except ItemNotFoundError: @@ -66,12 +63,10 @@ class CourseDetails: return course @classmethod - def update_from_json(cls, jsonval): + def update_from_json(cls, jsondict): """ Decode the json into CourseDetails and save any changed attrs to the db """ - jsondict = json.loads(jsonval) - ## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore course_location = jsondict['course_location'] ## Will probably want to cache the inflight courses because every blur generates an update @@ -79,38 +74,57 @@ class CourseDetails: dirty = False - ## FIXME do more accurate comparison (convert to time? or convert persisted from time) - if (jsondict['start_date'] != descriptor.start): + ## ??? Will this comparison work? + if 'start_date' in jsondict: + converted = jsdate_to_time(jsondict['start_date']) + else: + converted = None + if converted != descriptor.start: dirty = True - descriptor.start = jsondict['start_date'] + descriptor.start = converted - if (jsondict['end_date'] != descriptor.start): + if 'end_date' in jsondict: + converted = jsdate_to_time(jsondict['end_date']) + else: + converted = None + + if converted != descriptor.end: dirty = True - descriptor.end = jsondict['end_date'] + descriptor.end = converted - if (jsondict['enrollment_start'] != descriptor.enrollment_start): + if 'enrollment_start' in jsondict: + converted = jsdate_to_time(jsondict['enrollment_start']) + else: + converted = None + + if converted != descriptor.enrollment_start: dirty = True - descriptor.enrollment_start = jsondict['enrollment_start'] + descriptor.enrollment_start = converted - if (jsondict['enrollment_end'] != descriptor.enrollment_end): + if 'enrollment_end' in jsondict: + converted = jsdate_to_time(jsondict['enrollment_end']) + else: + converted = None + + if converted != descriptor.enrollment_end: dirty = True - descriptor.enrollment_end = jsondict['enrollment_end'] + descriptor.enrollment_end = converted if dirty: - modulestore('direct').update_item(course_location, descriptor.definition['data']) + modulestore('direct').update_metadata(course_location, descriptor.metadata) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. - temploc = course_location._replace(category='about', name='syllabus') + temploc = Location(course_location)._replace(category='about', name='syllabus') modulestore('direct').update_item(temploc, jsondict['syllabus']) - temploc = course_location._replace(name='overview') + temploc = temploc._replace(name='overview') modulestore('direct').update_item(temploc, jsondict['overview']) - temploc = course_location._replace(name='effort') + temploc = temploc._replace(name='effort') modulestore('direct').update_item(temploc, jsondict['effort']) - temploc = course_location._replace(name='video') + temploc = temploc._replace(name='video') modulestore('direct').update_item(temploc, jsondict['intro_video']) @@ -124,5 +138,7 @@ class CourseDetailsEncoder(json.JSONEncoder): return obj.__dict__ elif isinstance(obj, Location): return obj.dict() + elif isinstance(obj, time.struct_time): + return time_to_date(obj) else: - return JSONEncoder.default(self, obj) \ No newline at end of file + return JSONEncoder.default(self, obj) diff --git a/common/djangoapps/util/converters.py b/common/djangoapps/util/converters.py new file mode 100644 index 0000000000..200fab9766 --- /dev/null +++ b/common/djangoapps/util/converters.py @@ -0,0 +1,20 @@ +import time, datetime +import re + +def time_to_date(time_obj): + """ + Convert a time.time_struct to a true universal time (can pass to js Date constructor) + """ + return time.mktime(time_obj) * 1000 + +def jsdate_to_time(field): + """ + Convert a true universal time (msec since epoch) from a string to a time obj + """ + if field is None: + return field + elif isinstance(field, unicode): # iso format but ignores time zone assuming it's Z + d=datetime.datetime(*map(int, re.split('[^\d]', field)[:-1])) + return d.utctimetuple() + elif isinstance(field, int): + return time.gmtime(field / 1000) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 5b19d57dce..58adaa2d8d 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -90,10 +90,6 @@ class CourseDescriptor(SequenceDescriptor): log.critical(msg) system.error_tracker(msg) - self.enrollment_start = self._try_parse_time("enrollment_start") - self.enrollment_end = self._try_parse_time("enrollment_end") - self.end = self._try_parse_time("end") - # NOTE: relies on the modulestore to call set_grading_policy() right after # init. (Modulestore is in charge of figuring out where to load the policy from) @@ -249,6 +245,30 @@ class CourseDescriptor(SequenceDescriptor): def has_started(self): return time.gmtime() > self.start + @property + def end(self): + return self._try_parse_time("end") + @end.setter + def end(self, value): + if isinstance(value, time.struct_time): + self.metadata['end'] = stringify_time(value) + @property + def enrollment_start(self): + return self._try_parse_time("enrollment_start") + + @enrollment_start.setter + def enrollment_start(self, value): + if isinstance(value, time.struct_time): + self.metadata['enrollment_start'] = stringify_time(value) + @property + def enrollment_end(self): + return self._try_parse_time("enrollment_end") + + @enrollment_end.setter + def enrollment_end(self, value): + if isinstance(value, time.struct_time): + self.metadata['enrollment_end'] = stringify_time(value) + @property def grader(self): return self._grading_policy['GRADER'] diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 7668c1b1d1..94bad60cc8 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -10,10 +10,11 @@ from collections import namedtuple from pkg_resources import resource_listdir, resource_string, resource_isdir from xmodule.modulestore import Location -from xmodule.timeparse import parse_time +from xmodule.timeparse import parse_time, stringify_time from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX from xmodule.modulestore.exceptions import ItemNotFoundError +import time log = logging.getLogger('mitx.' + __name__) @@ -481,6 +482,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): return None return self._try_parse_time('start') + @start.setter + def start(self, value): + if isinstance(value, time.struct_time): + self.metadata['start'] = stringify_time(value) + @property def own_metadata(self): """