Commit 993d59b2 by cahrens

Merge branch 'feature/cas/speed-editor' of github.com:MITx/mitx into feature/cas/speed-editor

parents 58837066 70e53449
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import json import json
from cms.djangoapps.contentstore.course_info_model import update_course_updates
class CourseUpdateTest(CourseTestCase): class CourseUpdateTest(CourseTestCase):
def test_course_update(self): def test_course_update(self):
...@@ -14,14 +12,17 @@ class CourseUpdateTest(CourseTestCase): ...@@ -14,14 +12,17 @@ class CourseUpdateTest(CourseTestCase):
content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0"></iframe>' content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0"></iframe>'
payload = { 'content' : content, payload = { 'content' : content,
'date' : 'January 8, 2013'} 'date' : 'January 8, 2013'}
# No means to post w/ provided_id missing. django doesn't handle. So, go direct for the create
payload = update_course_updates(['i4x', self.course_location.org, self.course_location.course, 'course_info', "updates"] , payload)
url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
'provided_id' : payload['id']}) 'provided_id' : ''})
resp = self.client.post(url, json.dumps(payload), "application/json")
payload= json.loads(resp.content)
self.assertHTMLEqual(content, payload['content'], "single iframe") self.assertHTMLEqual(content, payload['content'], "single iframe")
url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
'provided_id' : payload['id']})
content += '<div>div <p>p</p></div>' content += '<div>div <p>p</p></div>'
payload['content'] = content payload['content'] = content
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
......
...@@ -975,6 +975,11 @@ def course_info_updates(request, org, course, provided_id=None): ...@@ -975,6 +975,11 @@ def course_info_updates(request, org, course, provided_id=None):
# ??? No way to check for access permission afaik # ??? No way to check for access permission afaik
# get current updates # get current updates
location = ['i4x', org, course, 'course_info', "updates"] location = ['i4x', org, course, 'course_info', "updates"]
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
# Possibly due to my removing the seemingly redundant pattern in urls.py
if provided_id == '':
provided_id = None
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
......
...@@ -56,7 +56,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -56,7 +56,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
event.preventDefault() event.preventDefault()
data = @module.save() data = @module.save()
data.metadata = _.extend(data.metadata, @metadata()) data.metadata = _.extend(data.metadata, @metadata())
$modalCover.hide() @hideModal()
@model.save(data).done( => @model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3) # # showToastMessage("Your changes have been saved.", null, 3)
@module = null @module = null
...@@ -70,11 +70,15 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -70,11 +70,15 @@ class CMS.Views.ModuleEdit extends Backbone.View
event.preventDefault() event.preventDefault()
@$el.removeClass('editing') @$el.removeClass('editing')
@$component_editor().slideUp(150) @$component_editor().slideUp(150)
@hideModal()
hideModal: ->
$modalCover.hide() $modalCover.hide()
$modalCover.removeClass('is-fixed')
clickEditButton: (event) -> clickEditButton: (event) ->
event.preventDefault() event.preventDefault()
@$el.addClass('editing') @$el.addClass('editing')
$modalCover.show() $modalCover.show().addClass('is-fixed')
@$component_editor().slideDown(150) @$component_editor().slideDown(150)
@loadEdit() @loadEdit()
...@@ -29,9 +29,7 @@ $(document).ready(function() { ...@@ -29,9 +29,7 @@ $(document).ready(function() {
$('.expand-collapse-icon').bind('click', toggleSubmodules); $('.expand-collapse-icon').bind('click', toggleSubmodules);
$('.visibility-options').bind('change', setVisibility); $('.visibility-options').bind('change', setVisibility);
$('.unit-history ol a').bind('click', showHistoryModal);
$modal.bind('click', hideModal); $modal.bind('click', hideModal);
$modalCover.bind('click', hideHistoryModal);
$modalCover.bind('click', hideModal); $modalCover.bind('click', hideModal);
$('.assets .upload-button').bind('click', showUploadModal); $('.assets .upload-button').bind('click', showUploadModal);
$('.upload-modal .close-button').bind('click', hideModal); $('.upload-modal .close-button').bind('click', hideModal);
...@@ -41,7 +39,13 @@ $(document).ready(function() { ...@@ -41,7 +39,13 @@ $(document).ready(function() {
$('.unit .item-actions .delete-button').bind('click', deleteUnit); $('.unit .item-actions .delete-button').bind('click', deleteUnit);
$('.new-unit-item').bind('click', createNewUnit); $('.new-unit-item').bind('click', createNewUnit);
$('.collapse-all-button').bind('click', collapseAll); // toggling overview section details
$(function(){
if($('.courseware-section').length > 0) {
$('.toggle-button-sections').addClass('is-shown');
}
});
$('.toggle-button-sections').bind('click', toggleSections);
// autosave when a field is updated on the subsection page // autosave when a field is updated on the subsection page
$body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue); $body.on('keyup', '.subsection-display-name-input, .unit-subtitle, .policy-list-value', checkForNewValue);
...@@ -125,9 +129,30 @@ $(document).ready(function() { ...@@ -125,9 +129,30 @@ $(document).ready(function() {
}); });
}); });
function collapseAll(e) { // function collapseAll(e) {
$('.branch').addClass('collapsed'); // $('.branch').addClass('collapsed');
$('.expand-collapse-icon').removeClass('collapse').addClass('expand'); // $('.expand-collapse-icon').removeClass('collapse').addClass('expand');
// }
function toggleSections(e) {
e.preventDefault();
$section = $('.courseware-section');
sectionCount = $section.length;
$button = $(this);
$labelCollapsed = $('<i class="ss-icon ss-symbolicons-block">up</i> <span class="label">Collapse All Sections</span>');
$labelExpanded = $('<i class="ss-icon ss-symbolicons-block">down</i> <span class="label">Expand All Sections</span>');
var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded;
$button.toggleClass('is-activated').html(buttonLabel);
if($button.hasClass('is-activated')) {
$section.addClass('collapsed');
$section.find('.expand-collapse-icon').removeClass('collapsed').addClass('expand');
} else {
$section.removeClass('collapsed');
$section.find('.expand-collapse-icon').removeClass('expand').addClass('collapse');
}
} }
function editSectionPublishDate(e) { function editSectionPublishDate(e) {
...@@ -498,9 +523,14 @@ function hideModal(e) { ...@@ -498,9 +523,14 @@ function hideModal(e) {
if(e) { if(e) {
e.preventDefault(); e.preventDefault();
} }
$('.file-input').unbind('change', startUpload); // Unit editors do not want the modal cover to hide when users click outside
$modal.hide(); // of the editor. Users must press Cancel or Save to exit the editor.
$modalCover.hide(); // module_edit adds and removes the "is-fixed" class.
if (!$modalCover.hasClass("is-fixed")) {
$('.file-input').unbind('change', startUpload);
$modal.hide();
$modalCover.hide();
}
} }
function onKeyUp(e) { function onKeyUp(e) {
...@@ -530,21 +560,6 @@ function closeComponentEditor(e) { ...@@ -530,21 +560,6 @@ function closeComponentEditor(e) {
$(this).closest('.xmodule_edit').removeClass('editing').find('.component-editor').slideUp(150); $(this).closest('.xmodule_edit').removeClass('editing').find('.component-editor').slideUp(150);
} }
function showHistoryModal(e) {
e.preventDefault();
$modal.show();
$modalCover.show();
}
function hideHistoryModal(e) {
e.preventDefault();
$modal.hide();
$modalCover.hide();
}
function showDateSetter(e) { function showDateSetter(e) {
e.preventDefault(); e.preventDefault();
var $block = $(this).closest('.due-date-input'); var $block = $(this).closest('.due-date-input');
......
...@@ -388,6 +388,9 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -388,6 +388,9 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
var graceEle = this.$el.find('#course-grading-graceperiod'); var graceEle = this.$el.find('#course-grading-graceperiod');
graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime
if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate()); if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate());
// remove any existing listeners to keep them from piling on b/c render gets called frequently
graceEle.off('change', this.setGracePeriod);
graceEle.on('change', this, this.setGracePeriod);
return this; return this;
}, },
...@@ -398,14 +401,16 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -398,14 +401,16 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
fieldToSelectorMap : { fieldToSelectorMap : {
'grace_period' : 'course-grading-graceperiod' 'grace_period' : 'course-grading-graceperiod'
}, },
setGracePeriod : function(event) {
event.data.clearValidationErrors();
var newVal = event.data.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal);
},
updateModel : function(event) { updateModel : function(event) {
if (!this.selectorToField[event.currentTarget.id]) return; if (!this.selectorToField[event.currentTarget.id]) return;
switch (this.selectorToField[event.currentTarget.id]) { switch (this.selectorToField[event.currentTarget.id]) {
case 'grace_period': case 'grace_period': // handled above
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; break;
default: default:
......
...@@ -422,6 +422,14 @@ input.courseware-unit-search-input { ...@@ -422,6 +422,14 @@ input.courseware-unit-search-input {
float: left; float: left;
margin: 29px 6px 16px 16px; margin: 29px 6px 16px 16px;
@include transition(none); @include transition(none);
&.expand {
background-position: 0 0;
}
&.collapsed {
}
} }
.drag-handle { .drag-handle {
...@@ -501,14 +509,31 @@ input.courseware-unit-search-input { ...@@ -501,14 +509,31 @@ input.courseware-unit-search-input {
} }
} }
.collapse-all-button { .toggle-button-sections {
display: none;
position: relative;
float: right; float: right;
margin-top: 10px; margin-top: 10px;
font-size: 13px; font-size: 13px;
color: $darkGrey; color: $darkGrey;
.collapse-all-icon { &.is-shown {
margin-right: 6px; display: block;
}
.ss-icon {
@include border-radius(20px);
position: relative;
top: -1px;
display: inline-block;
margin-right: 2px;
line-height: 5px;
font-size: 11px;
}
.label {
display: inline-block;
} }
} }
......
...@@ -245,8 +245,8 @@ ...@@ -245,8 +245,8 @@
&.editing { &.editing {
border: 1px solid $lightBluishGrey2; border: 1px solid $lightBluishGrey2;
z-index: 9999; z-index: auto;
.drag-handle, .drag-handle,
.component-actions { .component-actions {
display: none; display: none;
...@@ -284,12 +284,17 @@ ...@@ -284,12 +284,17 @@
overflow-x: auto; overflow-x: auto;
} }
.wrapper-component-editor {
z-index: 9999;
position: relative;
}
.component-editor { .component-editor {
@include edit-box; @include edit-box;
@include box-shadow(none);
display: none; display: none;
padding: 20px; padding: 20px;
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
@include box-shadow(none);
.metadata_edit { .metadata_edit {
margin-bottom: 20px; margin-bottom: 20px;
......
<div class="component-editor"> <div class="wrapper wrapper-component-editor">
<div class="module-editor"> <div class="component-editor">
${editor} <div class="module-editor">
</div> ${editor}
<a href="#" class="save-button">Save</a> </div>
<a href="#" class="cancel-button">Cancel</a> <a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a>
</div>
</div> </div>
<div class="component-actions"> <div class="component-actions">
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a> <a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a> <a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a>
</div> </div>
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a> <a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
${preview} ${preview}
\ No newline at end of file
...@@ -122,7 +122,7 @@ ...@@ -122,7 +122,7 @@
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="page-actions"> <div class="page-actions">
<a href="#" class="new-button new-courseware-section-button"><span class="plus-icon white"></span> New Section</a> <a href="#" class="new-button new-courseware-section-button"><span class="plus-icon white"></span> New Section</a>
<a href="#" class="collapse-all-button"><span class="collapse-all-icon"></span>Collapse All</a> <a href="#" class="toggle-button toggle-button-sections"><i class="ss-icon ss-symbolicons-block">up</i> <span class="label">Collapse All Sections</span></a>
</div> </div>
<article class="courseware-overview" data-course-id="${context_course.location.url()}"> <article class="courseware-overview" data-course-id="${context_course.location.url()}">
% for section in sections: % for section in sections:
......
...@@ -6,6 +6,7 @@ from xmodule.modulestore import Location ...@@ -6,6 +6,7 @@ from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time, stringify_time from xmodule.timeparse import parse_time, stringify_time
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from datetime import datetime
import json import json
import logging import logging
import requests import requests
...@@ -18,6 +19,8 @@ log = logging.getLogger(__name__) ...@@ -18,6 +19,8 @@ log = logging.getLogger(__name__)
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True) remove_comments=True, remove_blank_text=True)
_cached_toc = {}
class CourseDescriptor(SequenceDescriptor): class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule module_class = SequenceModule
...@@ -50,6 +53,24 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -50,6 +53,24 @@ class CourseDescriptor(SequenceDescriptor):
""" """
toc_url = self.book_url + 'toc.xml' toc_url = self.book_url + 'toc.xml'
# cdodge: I've added this caching of TOC because in Mongo-backed instances (but not Filesystem stores)
# course modules have a very short lifespan and are constantly being created and torn down.
# Since this module in the __init__() method does a synchronous call to AWS to get the TOC
# this is causing a big performance problem. So let's be a bit smarter about this and cache
# each fetch and store in-mem for 10 minutes.
# NOTE: I have to get this onto sandbox ASAP as we're having runtime failures. I'd like to swing back and
# rewrite to use the traditional Django in-memory cache.
try:
# see if we already fetched this
if toc_url in _cached_toc:
(table_of_contents, timestamp) = _cached_toc[toc_url]
age = datetime.now() - timestamp
# expire every 10 minutes
if age.seconds < 600:
return table_of_contents
except Exception as err:
pass
# Get the table of contents from S3 # Get the table of contents from S3
log.info("Retrieving textbook table of contents from %s" % toc_url) log.info("Retrieving textbook table of contents from %s" % toc_url)
try: try:
...@@ -62,6 +83,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -62,6 +83,7 @@ class CourseDescriptor(SequenceDescriptor):
# TOC is XML. Parse it # TOC is XML. Parse it
try: try:
table_of_contents = etree.fromstring(r.text) table_of_contents = etree.fromstring(r.text)
_cached_toc[toc_url] = (table_of_contents, datetime.now())
except Exception as err: except Exception as err:
msg = 'Error %s: Unable to parse XML for textbook table of contents at %s' % (err, toc_url) msg = 'Error %s: Unable to parse XML for textbook table of contents at %s' % (err, toc_url)
log.error(msg) log.error(msg)
......
...@@ -284,7 +284,7 @@ class ModuleStore(object): ...@@ -284,7 +284,7 @@ class ModuleStore(object):
""" """
raise NotImplementedError raise NotImplementedError
def get_items(self, location, depth=0): def get_items(self, location, course_id=None, depth=0):
""" """
Returns a list of XModuleDescriptor instances for the items Returns a list of XModuleDescriptor instances for the items
that match location. Any element of location that is None is treated that match location. Any element of location that is None is treated
......
...@@ -262,7 +262,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -262,7 +262,7 @@ class MongoModuleStore(ModuleStoreBase):
""" """
return self.get_item(location) return self.get_item(location)
def get_items(self, location, depth=0): def get_items(self, location, course_id=None, depth=0):
items = self.collection.find( items = self.collection.find(
location_to_query(location), location_to_query(location),
sort=[('revision', pymongo.ASCENDING)], sort=[('revision', pymongo.ASCENDING)],
......
...@@ -513,16 +513,22 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -513,16 +513,22 @@ class XMLModuleStore(ModuleStoreBase):
raise NotImplementedError("XMLModuleStores can't guarantee that definitions" raise NotImplementedError("XMLModuleStores can't guarantee that definitions"
" are unique. Use get_instance.") " are unique. Use get_instance.")
def get_items(self, location, depth=0): def get_items(self, location, course_id=None, depth=0):
items = [] items = []
for _, modules in self.modules.iteritems():
for mod_loc, module in modules.iteritems():
def _add_get_items(self, location, modules):
for mod_loc, module in modules.iteritems():
# Locations match if each value in `location` is None or if the value from `location` # Locations match if each value in `location` is None or if the value from `location`
# matches the value from `mod_loc` # matches the value from `mod_loc`
if all(goal is None or goal == value for goal, value in zip(location, mod_loc)): if all(goal is None or goal == value for goal, value in zip(location, mod_loc)):
items.append(module) items.append(module)
if course_id is None:
for _, modules in self.modules.iteritems():
_add_get_items(self, location, modules)
else:
_add_get_items(self, location, self.modules[course_id])
return items return items
......
...@@ -120,47 +120,16 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -120,47 +120,16 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_data_path = None course_data_path = None
course_location = None course_location = None
# Quick scan to get course Location as well as the course_data_path
# Quick scan to get course module as we need some info from there. Also we need to make sure that the
# course module is committed first into the store
for module in module_store.modules[course_id].itervalues(): for module in module_store.modules[course_id].itervalues():
if module.category == 'course': if module.category == 'course':
course_data_path = path(data_dir) / module.metadata['data_dir'] course_data_path = path(data_dir) / module.metadata['data_dir']
course_location = module.location course_location = module.location
if static_content_store is not None: module = remap_namespace(module, target_location_namespace)
_namespace_rename = target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location
# first pass to find everything in /static/
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
_namespace_rename, subpath='static')
for module in module_store.modules[course_id].itervalues():
# remap module to the new namespace
if target_location_namespace is not None:
# This looks a bit wonky as we need to also change the 'name' of the imported course to be what
# the caller passed in
if module.location.category != 'course':
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course)
else:
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course, name=target_location_namespace.name)
# then remap children pointers since they too will be re-namespaced
children_locs = module.definition.get('children')
if children_locs is not None:
new_locs = []
for child in children_locs:
child_loc = Location(child)
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course)
new_locs.append(new_child_loc.url())
module.definition['children'] = new_locs
if module.category == 'course':
# HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this. # HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
module.metadata['hide_progress_tab'] = True module.metadata['hide_progress_tab'] = True
...@@ -174,12 +143,41 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -174,12 +143,41 @@ def import_from_xml(store, data_dir, course_dirs=None,
{"type": "discussion", "name": "Discussion"}, {"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
store.update_item(module.location, module.definition['data'])
if 'children' in module.definition:
store.update_children(module.location, module.definition['children'])
store.update_metadata(module.location, dict(module.own_metadata))
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
# so let's make sure we import in case there are no other references to it in the modules # so let's make sure we import in case there are no other references to it in the modules
verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg') verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
course_items.append(module) course_items.append(module)
# then import all the static content
if static_content_store is not None:
_namespace_rename = target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location
# first pass to find everything in /static/
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
_namespace_rename, subpath='static')
# finally loop through all the modules
for module in module_store.modules[course_id].itervalues():
if module.category == 'course':
# we've already saved the course module up at the top of the loop
# so just skip over it in the inner loop
continue
# remap module to the new namespace
if target_location_namespace is not None:
module = remap_namespace(module, target_location_namespace)
if 'data' in module.definition: if 'data' in module.definition:
module_data = module.definition['data'] module_data = module.definition['data']
...@@ -216,6 +214,33 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -216,6 +214,33 @@ def import_from_xml(store, data_dir, course_dirs=None,
return module_store, course_items return module_store, course_items
def remap_namespace(module, target_location_namespace):
if target_location_namespace is None:
return module
# This looks a bit wonky as we need to also change the 'name' of the imported course to be what
# the caller passed in
if module.location.category != 'course':
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course)
else:
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course, name=target_location_namespace.name)
# then remap children pointers since they too will be re-namespaced
children_locs = module.definition.get('children')
if children_locs is not None:
new_locs = []
for child in children_locs:
child_loc = Location(child)
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
course=target_location_namespace.course)
new_locs.append(new_child_loc.url())
module.definition['children'] = new_locs
return module
def validate_category_hierarcy(module_store, course_id, parent_category, expected_child_category): def validate_category_hierarcy(module_store, course_id, parent_category, expected_child_category):
err_cnt = 0 err_cnt = 0
......
.ui-timepicker-list { .ui-timepicker-list {
overflow-y: auto; overflow-y: auto;
height: 150px; height: 150px;
width: 7.5em; width: 6.5em;
background: #fff; background: #fff;
border: 1px solid #8891a1; border: 1px solid #ddd;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,0.1); -webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);
-moz-box-shadow: 0 5px 10px rgba(0,0,0,0.1); -moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);
box-shadow: 0 5px 10px rgba(0,0,0,0.1); box-shadow:0 5px 10px rgba(0,0,0,0.2);
outline: none; outline: none;
z-index: 100001; z-index: 10001;
font-size: 12px;
} }
.ui-timepicker-list.ui-timepicker-with-duration { .ui-timepicker-list.ui-timepicker-with-duration {
......
...@@ -127,6 +127,7 @@ def sort_map_entries(category_map): ...@@ -127,6 +127,7 @@ def sort_map_entries(category_map):
sort_map_entries(category_map["subcategories"][title]) sort_map_entries(category_map["subcategories"][title])
category_map["children"] = [x[0] for x in sorted(things, key=lambda x: x[1]["sort_key"])] category_map["children"] = [x[0] for x in sorted(things, key=lambda x: x[1]["sort_key"])]
def initialize_discussion_info(course): def initialize_discussion_info(course):
global _DISCUSSIONINFO global _DISCUSSIONINFO
...@@ -134,32 +135,32 @@ def initialize_discussion_info(course): ...@@ -134,32 +135,32 @@ def initialize_discussion_info(course):
return return
course_id = course.id course_id = course.id
all_modules = get_full_modules()[course_id]
discussion_id_map = {} discussion_id_map = {}
unexpanded_category_map = defaultdict(list) unexpanded_category_map = defaultdict(list)
for location, module in all_modules.items(): # get all discussion models within this course_id
if location.category == 'discussion': all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id)
skip_module = False
for key in ('id', 'discussion_category', 'for'): for module in all_modules:
if key not in module.metadata: skip_module = False
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location)) for key in ('id', 'discussion_category', 'for'):
skip_module = True if key not in module.metadata:
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
if skip_module: skip_module = True
continue
if skip_module:
id = module.metadata['id'] continue
category = module.metadata['discussion_category']
title = module.metadata['for'] id = module.metadata['id']
sort_key = module.metadata.get('sort_key', title) category = module.metadata['discussion_category']
category = " / ".join([x.strip() for x in category.split("/")]) title = module.metadata['for']
last_category = category.split("/")[-1] sort_key = module.metadata.get('sort_key', title)
discussion_id_map[id] = {"location": location, "title": last_category + " / " + title} category = " / ".join([x.strip() for x in category.split("/")])
unexpanded_category_map[category].append({"title": title, "id": id, last_category = category.split("/")[-1]
"sort_key": sort_key, "start_date": module.start}) discussion_id_map[id] = {"location": module.location, "title": last_category + " / " + title}
unexpanded_category_map[category].append({"title": title, "id": id,
"sort_key": sort_key, "start_date": module.start})
category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)} category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)}
for category_path, entries in unexpanded_category_map.items(): for category_path, entries in unexpanded_category_map.items():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment