Beginning test of details tab.
This commit is contained in:
@@ -44,6 +44,8 @@ import sys
|
||||
import tarfile
|
||||
import time
|
||||
from contentstore import course_info_model
|
||||
from models.settings.course_details import CourseDetails
|
||||
from models.settings.course_details import CourseDetailsEncoder
|
||||
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
|
||||
@@ -871,9 +873,6 @@ def edit_static(request, org, course, coursename):
|
||||
return render_to_response('edit-static-page.html', {})
|
||||
|
||||
|
||||
def settings(request, org, course, coursename):
|
||||
return render_to_response('settings.html', {})
|
||||
|
||||
def edit_tabs(request, org, course, coursename):
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
course_item = modulestore().get_item(location)
|
||||
@@ -949,13 +948,58 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(course_info_model.get_course_updates(location)), mimetype="application/json")
|
||||
elif real_method == 'POST':
|
||||
# new instance (unless django makes PUT a POST): updates are coming as POST. Not sure why.
|
||||
return HttpResponse(json.dumps(course_info_model.update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
elif real_method == 'PUT':
|
||||
return HttpResponse(json.dumps(course_info_model.update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
|
||||
elif real_method == 'DELETE':
|
||||
return HttpResponse(json.dumps(course_info_model.delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def get_course_settings(request, org, course, name):
|
||||
"""
|
||||
Send models and views as well as html for editing the course settings to the client.
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
return render_to_response('settings.html', {
|
||||
'active_tab': 'settings-tab',
|
||||
'context_course': course_module,
|
||||
'course_details' : json.dumps(CourseDetails.fetch(location), cls=CourseDetailsEncoder)
|
||||
})
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_settings_updates(request, org, course, name, section):
|
||||
"""
|
||||
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
|
||||
through json (not rendering any html) and handles section level operations rather than whole page.
|
||||
|
||||
org, course: Attributes of the Location for the item to edit
|
||||
section: one of details, faculty, grading, problems, discussions
|
||||
"""
|
||||
if section == 'details':
|
||||
manager = CourseDetails
|
||||
else: return
|
||||
|
||||
if request.method == 'GET':
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseDetailsEncoder),
|
||||
mimetype="application/json")
|
||||
elif request.method == 'POST': # post or put, doesn't matter.
|
||||
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseDetailsEncoder),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def asset_index(request, org, course, name):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
CMS.Models.Settings.CourseDetails = Backbone.Models.extend({
|
||||
defaults: {
|
||||
location : null, # a Location model, required
|
||||
start_date: null, # maps to 'start'
|
||||
end_date: null, # maps to 'end'
|
||||
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,
|
||||
@@ -19,6 +19,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Models.extend({
|
||||
},
|
||||
|
||||
urlRoot: function() {
|
||||
// TODO impl
|
||||
var location = this.get('location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ CMS.Views.Settings.Main = Backbone.View.extend({
|
||||
// allow navigation between the tabs
|
||||
events: {
|
||||
'click .settings-page-menu a': "showSettingsTab",
|
||||
'blur input' : 'updateModel'
|
||||
},
|
||||
|
||||
currentTab: null,
|
||||
@@ -72,19 +71,50 @@ CMS.Views.Settings.Main = Backbone.View.extend({
|
||||
CMS.Views.Settings.Details = Backbone.View.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
events : {
|
||||
"blur input" : "updateModel"
|
||||
"blur input" : "updateModel",
|
||||
'click .remove-course-syllabus' : "removeSyllabus",
|
||||
'click .new-course-syllabus' : 'assetSyllabus',
|
||||
'click .remove-course-introduction-video' : "removeVideo",
|
||||
'click .new-course-introduction-video' : 'assetVideo',
|
||||
},
|
||||
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'));
|
||||
|
||||
if (this.model.has('syllabus')) {
|
||||
this.$el.find('.current-course-syllabus .doc-filename').html(
|
||||
this.fileAnchorTemplate({
|
||||
fullpath : this.model.get('syllabus'),
|
||||
filename: 'syllabus'}));
|
||||
this.$el.find('.remove-course-syllabus').show();
|
||||
}
|
||||
else this.$el.find('.remove-course-syllabus').hide();
|
||||
|
||||
if (this.model.has('overview'))
|
||||
this.$el.find('#course-overview').text(this.model.get('overview'));
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
updateModel: function(event) {
|
||||
// figure out which field
|
||||
switch (event.currentTarget.id) {
|
||||
case 'course-start-date':
|
||||
this.model.set('start_date', $(event.currentTarget).datepicker('getDate'));
|
||||
var val = $(event.currentTarget).datepicker('getDate');
|
||||
this.model.set('start_date', val);
|
||||
break;
|
||||
case 'course-end-date':
|
||||
this.model.set('end_date', $(event.currentTarget).datepicker('getDate'));
|
||||
@@ -96,10 +126,28 @@ CMS.Views.Settings.Details = Backbone.View.extend({
|
||||
this.model.set('enrollment_end', $(event.currentTarget).datepicker('getDate'));
|
||||
break;
|
||||
|
||||
case 'course-overview':
|
||||
this.model.set('overview', $(event.currentTarget).text());
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// save the updated model
|
||||
this.model.save();
|
||||
},
|
||||
|
||||
removeSyllabus: function() {
|
||||
if (this.model.has('syllabus')) this.model.set({'syllabus': null});
|
||||
},
|
||||
|
||||
assetSyllabus : function() {
|
||||
// TODO implement
|
||||
},
|
||||
|
||||
removeVideo: function() {
|
||||
if (this.model.has('intro_video')) this.model.set({'intro_video': null});
|
||||
},
|
||||
|
||||
assetVideo : function() {
|
||||
// TODO implement
|
||||
}
|
||||
});
|
||||
@@ -28,8 +28,14 @@
|
||||
el: $('.main-wrapper'),
|
||||
model : settingsModel)
|
||||
});
|
||||
$('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
|
||||
|
||||
$('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
|
||||
|
||||
$(":input, textarea").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
editor.render();
|
||||
});
|
||||
|
||||
@@ -42,11 +48,6 @@
|
||||
var gradeThresholds;
|
||||
var GRADES = ['A', 'B', 'C', 'D', 'E'];
|
||||
|
||||
$(" :input, textarea").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
(function() {
|
||||
$body = $('body');
|
||||
@@ -146,7 +147,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="[COURSE_SUMMARY_URL]">your course URL</a>, and cannot be changed</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,8 +226,8 @@
|
||||
<label for="course-syllabus">Course Syllabus</label>
|
||||
<div class="field">
|
||||
<div class="input input-existing">
|
||||
<div class=" current current-course-syllabus">
|
||||
<span class="doc-filename"><a href="[link to file]".pdf> <i class="ss-icon ss-standard">📄</i>CS184x_syllabus.pdf</a></span>
|
||||
<div class="current current-course-syllabus">
|
||||
<span class="doc-filename"></span>
|
||||
|
||||
<a href="#" class="remove-item remove-course-syllabus remove-doc-data" id="course-syllabus"><span class="delete-icon"></span> Delete Syllabus</a>
|
||||
</div>
|
||||
@@ -255,7 +256,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="[COURSE_SUMMARY_URL]">your course summary page</a></span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,8 +265,8 @@
|
||||
<label for="course-introduction-video">Introduction Video:</label>
|
||||
<div class="field">
|
||||
<div class="input input-existing">
|
||||
<div class=" current current-course-introduction-video">
|
||||
<iframe width="380" height="215" src="http://www.youtube.com/embed/6F0pR-ANmXY" frameborder="0" allowfullscreen></iframe>
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="380" height="215" src="" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Video</a>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<li><a href="${reverse('edit_tabs', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}" id='pages-tab'>Tabs</a></li>
|
||||
<li><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='assets-tab'>Assets</a></li>
|
||||
<li><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}" id='users-tab'>Users</a></li>
|
||||
<li><a href="${reverse('course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='settings-tab'>Settings</a></li>
|
||||
<li><a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='import-tab'>Import</a></li>
|
||||
</ul>
|
||||
% endif
|
||||
|
||||
@@ -36,14 +36,13 @@ urlpatterns = ('',
|
||||
'contentstore.views.remove_user', name='remove_user'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$', 'contentstore.views.course_info', name='course_info'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)$', 'contentstore.views.course_settings', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_settings/updates/(?P<provided_id>.*)$', 'contentstore.views.course_settings_updates', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
|
||||
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages',
|
||||
name='static_pages'),
|
||||
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
|
||||
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
|
||||
url(r'^settings/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.settings', name='settings'),
|
||||
|
||||
# temporary landing page for a course
|
||||
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'),
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
### A basic question is whether to break the details into schedule, intro, requirements, and misc sub objects
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
import json
|
||||
from json.encoder import JSONEncoder
|
||||
|
||||
class CourseDetails:
|
||||
def __init__(self, location):
|
||||
self.course_location = location # a Location obj
|
||||
@@ -16,9 +22,107 @@ class CourseDetails:
|
||||
"""
|
||||
Fetch the course details for the given course from persistence and return a CourseDetails model.
|
||||
"""
|
||||
if not isinstance(course_location, Location):
|
||||
course_location = Location(course_location)
|
||||
|
||||
course = cls(course_location)
|
||||
|
||||
# TODO implement
|
||||
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
|
||||
course.enrollment_end = descriptor.enrollment_end
|
||||
|
||||
temploc = course_location._replace(category='about', name='syllabus')
|
||||
try:
|
||||
course.syllabus = modulestore('direct').get_item(temploc).definition['data']
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = course_location._replace(name='overview')
|
||||
try:
|
||||
course.overview = modulestore('direct').get_item(temploc).definition['data']
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = course_location._replace(name='effort')
|
||||
try:
|
||||
course.effort = modulestore('direct').get_item(temploc).definition['data']
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = course_location._replace(name='video')
|
||||
try:
|
||||
course.intro_video = modulestore('direct').get_item(temploc).definition['data']
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
return course
|
||||
|
||||
@classmethod
|
||||
def update_from_json(cls, jsonval):
|
||||
"""
|
||||
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
|
||||
descriptor = modulestore('direct').get_item(course_location)
|
||||
|
||||
dirty = False
|
||||
|
||||
## FIXME do more accurate comparison (convert to time? or convert persisted from time)
|
||||
if (jsondict['start_date'] != descriptor.start):
|
||||
dirty = True
|
||||
descriptor.start = jsondict['start_date']
|
||||
|
||||
if (jsondict['end_date'] != descriptor.start):
|
||||
dirty = True
|
||||
descriptor.end = jsondict['end_date']
|
||||
|
||||
if (jsondict['enrollment_start'] != descriptor.enrollment_start):
|
||||
dirty = True
|
||||
descriptor.enrollment_start = jsondict['enrollment_start']
|
||||
|
||||
if (jsondict['enrollment_end'] != descriptor.enrollment_end):
|
||||
dirty = True
|
||||
descriptor.enrollment_end = jsondict['enrollment_end']
|
||||
|
||||
if dirty:
|
||||
modulestore('direct').update_item(course_location, descriptor.definition['data'])
|
||||
|
||||
# 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')
|
||||
modulestore('direct').update_item(temploc, jsondict['syllabus'])
|
||||
|
||||
temploc = course_location._replace(name='overview')
|
||||
modulestore('direct').update_item(temploc, jsondict['overview'])
|
||||
|
||||
temploc = course_location._replace(name='effort')
|
||||
modulestore('direct').update_item(temploc, jsondict['effort'])
|
||||
|
||||
temploc = course_location._replace(name='video')
|
||||
modulestore('direct').update_item(temploc, jsondict['intro_video'])
|
||||
|
||||
|
||||
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
|
||||
# it persisted correctly
|
||||
return CourseDetails.fetch(course_location)
|
||||
|
||||
class CourseDetailsEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, CourseDetails):
|
||||
return obj.__dict__
|
||||
elif isinstance(obj, Location):
|
||||
return obj.dict()
|
||||
else:
|
||||
return JSONEncoder.default(self, obj)
|
||||
Reference in New Issue
Block a user