Details tab works except for file references
This commit is contained in:
@@ -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
|
||||
});
|
||||
40
cms/static/js/models/settings/course_details.js
Normal file
40
cms/static/js/models/settings/course_details.js
Normal file
@@ -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';
|
||||
}
|
||||
});
|
||||
@@ -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';
|
||||
}
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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('<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>');
|
||||
// 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() {
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
<%block name="title">Settings</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
@@ -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 @@
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${get_lms_link_for_item(context_course.location,True)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_item(context_course.location, True)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,7 +161,7 @@
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${get_lms_link_for_item(context_course.location,True)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_item(context_course.location, True)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,7 +172,7 @@
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled">
|
||||
<span class="tip tip-inline">e.g. 101x</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${get_lms_link_for_item(context_course.location,True)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_item(context_course.location, True)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,7 +260,7 @@
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<textarea class="long tall edit-box tinymce" id="course-overview"></textarea>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${get_lms_link_for_item(context_course.location,True)}">your course summary page</a></span>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_item(context_course.location, True)}">your course summary page</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
return JSONEncoder.default(self, obj)
|
||||
|
||||
20
common/djangoapps/util/converters.py
Normal file
20
common/djangoapps/util/converters.py
Normal file
@@ -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)
|
||||
@@ -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']
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user