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")
......
...@@ -976,6 +976,11 @@ def course_info_updates(request, org, course, provided_id=None): ...@@ -976,6 +976,11 @@ def course_info_updates(request, org, course, provided_id=None):
# 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):
raise PermissionDenied() raise PermissionDenied()
......
...@@ -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();
} }
// Unit editors do not want the modal cover to hide when users click outside
// of the editor. Users must press Cancel or Save to exit the editor.
// module_edit adds and removes the "is-fixed" class.
if (!$modalCover.hasClass("is-fixed")) {
$('.file-input').unbind('change', startUpload); $('.file-input').unbind('change', startUpload);
$modal.hide(); $modal.hide();
$modalCover.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,7 +245,7 @@ ...@@ -245,7 +245,7 @@
&.editing { &.editing {
border: 1px solid $lightBluishGrey2; border: 1px solid $lightBluishGrey2;
z-index: 9999; z-index: auto;
.drag-handle, .drag-handle,
.component-actions { .component-actions {
...@@ -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="component-editor">
<div class="module-editor"> <div class="module-editor">
${editor} ${editor}
</div> </div>
<a href="#" class="save-button">Save</a> <a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</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}
...@@ -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 {
......
...@@ -2,16 +2,22 @@ ...@@ -2,16 +2,22 @@
jquery-timepicker jquery-timepicker
http://jonthornton.github.com/jquery-timepicker/ http://jonthornton.github.com/jquery-timepicker/
requires jQuery 1.6+ requires jQuery 1.7+
version: 1.2.2
************************/ ************************/
!(function($) (function (factory) {
{ if (typeof define === 'function' && define.amd) {
var _baseDate = new Date(); _baseDate.setHours(0); _baseDate.setMinutes(0); _baseDate.setSeconds(0); // AMD. Register as an anonymous module.
define(['jquery'], factory);
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
var _baseDate = _generateBaseDate();
var _ONE_DAY = 86400; var _ONE_DAY = 86400;
var _closeEvent = 'ontouchstart' in document ? 'touchstart' : 'mousedown';
var _defaults = { var _defaults = {
className: null, className: null,
minTime: null, minTime: null,
...@@ -22,7 +28,9 @@ version: 1.2.2 ...@@ -22,7 +28,9 @@ version: 1.2.2
timeFormat: 'g:ia', timeFormat: 'g:ia',
scrollDefaultNow: false, scrollDefaultNow: false,
scrollDefaultTime: false, scrollDefaultTime: false,
selectOnBlur: false selectOnBlur: false,
forceRoundTime: false,
appendTo: 'body'
}; };
var _lang = { var _lang = {
decimal: '.', decimal: '.',
...@@ -30,6 +38,7 @@ version: 1.2.2 ...@@ -30,6 +38,7 @@ version: 1.2.2
hr: 'hr', hr: 'hr',
hrs: 'hrs' hrs: 'hrs'
}; };
var globalInit = false;
var methods = var methods =
{ {
...@@ -78,31 +87,45 @@ version: 1.2.2 ...@@ -78,31 +87,45 @@ version: 1.2.2
self.data('timepicker-settings', settings); self.data('timepicker-settings', settings);
self.attr('autocomplete', 'off'); self.attr('autocomplete', 'off');
self.click(methods.show).focus(methods.show).blur(_formatValue).keydown(_keyhandler); self.on('click.timepicker focus.timepicker', methods.show);
self.on('blur.timepicker', _formatValue);
self.on('keydown.timepicker', _keyhandler);
self.addClass('ui-timepicker-input'); self.addClass('ui-timepicker-input');
if (self.val()) { _formatValue.call(self.get(0));
var prettyTime = _int2time(_time2int(self.val()), settings.timeFormat);
self.val(prettyTime);
}
if (!globalInit) {
// close the dropdown when container loses focus // close the dropdown when container loses focus
$("body").attr("tabindex", -1).focusin(function(e) { $('body').on(_closeEvent, function(e) {
if ($(e.target).closest('.ui-timepicker-input').length == 0 && $(e.target).closest('.ui-timepicker-list').length == 0) { var target = $(e.target);
var input = target.closest('.ui-timepicker-input');
if (input.length === 0 && target.closest('.ui-timepicker-list').length === 0) {
methods.hide(); methods.hide();
} }
}); });
globalInit = true;
}
}); });
}, },
show: function(e) show: function(e)
{ {
var self = $(this); var self = $(this);
if ('ontouchstart' in document) {
// block the keyboard on mobile devices
self.blur();
}
var list = self.data('timepicker-list'); var list = self.data('timepicker-list');
// check if input is readonly
if (self.attr('readonly')) {
return;
}
// check if list needs to be rendered // check if list needs to be rendered
if (!list || list.length == 0) { if (!list || list.length === 0) {
_render(self); _render(self);
list = self.data('timepicker-list'); list = self.data('timepicker-list');
} }
...@@ -121,16 +144,12 @@ version: 1.2.2 ...@@ -121,16 +144,12 @@ version: 1.2.2
// make sure other pickers are hidden // make sure other pickers are hidden
methods.hide(); methods.hide();
var topMargin = parseInt(self.css('marginTop').slice(0, -2));
if (!topMargin) topMargin = 0; // correct for IE returning "auto"
if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) { if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) {
// position the dropdown on top // position the dropdown on top
list.css({ 'left':(self.offset().left), 'top': self.offset().top + topMargin - list.outerHeight() }); list.css({ 'left':(self.offset().left), 'top': self.offset().top - list.outerHeight() });
} else { } else {
// put it under the input // put it under the input
list.css({ 'left':(self.offset().left), 'top': self.offset().top + topMargin + self.outerHeight() }); list.css({ 'left':(self.offset().left), 'top': self.offset().top + self.outerHeight() });
} }
list.show(); list.show();
...@@ -142,7 +161,7 @@ version: 1.2.2 ...@@ -142,7 +161,7 @@ version: 1.2.2
if (!selected.length) { if (!selected.length) {
if (self.val()) { if (self.val()) {
selected = _findRow(self, list, _time2int(self.val())); selected = _findRow(self, list, _time2int(self.val()));
} else if (settings.minTime === null && settings.scrollDefaultNow) { } else if (settings.scrollDefaultNow) {
selected = _findRow(self, list, _time2int(new Date())); selected = _findRow(self, list, _time2int(new Date()));
} else if (settings.scrollDefaultTime !== false) { } else if (settings.scrollDefaultTime !== false) {
selected = _findRow(self, list, _time2int(settings.scrollDefaultTime)); selected = _findRow(self, list, _time2int(settings.scrollDefaultTime));
...@@ -165,7 +184,8 @@ version: 1.2.2 ...@@ -165,7 +184,8 @@ version: 1.2.2
var list = $(this); var list = $(this);
var self = list.data('timepicker-input'); var self = list.data('timepicker-input');
var settings = self.data('timepicker-settings'); var settings = self.data('timepicker-settings');
if (settings.selectOnBlur) {
if (settings && settings.selectOnBlur) {
_selectValue(self); _selectValue(self);
} }
...@@ -226,8 +246,29 @@ version: 1.2.2 ...@@ -226,8 +246,29 @@ version: 1.2.2
var self = $(this); var self = $(this);
var prettyTime = _int2time(_time2int(value), self.data('timepicker-settings').timeFormat); var prettyTime = _int2time(_time2int(value), self.data('timepicker-settings').timeFormat);
self.val(prettyTime); self.val(prettyTime);
},
remove: function()
{
var self = $(this);
// check if this element is a timepicker
if (!self.hasClass('ui-timepicker-input')) {
return;
}
self.removeAttr('autocomplete', 'off');
self.removeClass('ui-timepicker-input');
self.removeData('timepicker-settings');
self.off('.timepicker');
// timepicker-list won't be present unless the user has interacted with this timepicker
if (self.data('timepicker-list')) {
self.data('timepicker-list').remove();
} }
self.removeData('timepicker-list');
}
}; };
// private methods // private methods
...@@ -251,7 +292,7 @@ version: 1.2.2 ...@@ -251,7 +292,7 @@ version: 1.2.2
list.css({'display':'none', 'position': 'absolute' }); list.css({'display':'none', 'position': 'absolute' });
if (settings.minTime !== null && settings.showDuration) { if ((settings.minTime !== null || settings.durationTime !== null) && settings.showDuration) {
list.addClass('ui-timepicker-with-duration'); list.addClass('ui-timepicker-with-duration');
} }
...@@ -267,14 +308,14 @@ version: 1.2.2 ...@@ -267,14 +308,14 @@ version: 1.2.2
for (var i=start; i <= end; i += settings.step*60) { for (var i=start; i <= end; i += settings.step*60) {
var timeInt = i%_ONE_DAY; var timeInt = i%_ONE_DAY;
var row = $('<li />'); var row = $('<li />');
row.data('time', timeInt) row.data('time', timeInt);
row.text(_int2time(timeInt, settings.timeFormat)); row.text(_int2time(timeInt, settings.timeFormat));
if (settings.minTime !== null && settings.showDuration) { if ((settings.minTime !== null || settings.durationTime !== null) && settings.showDuration) {
var duration = $('<span />'); var duration = $('<span />');
duration.addClass('ui-timepicker-duration'); duration.addClass('ui-timepicker-duration');
duration.text(' ('+_int2duration(i - durStart)+')'); duration.text(' ('+_int2duration(i - durStart)+')');
row.append(duration) row.append(duration);
} }
list.append(row); list.append(row);
...@@ -283,10 +324,16 @@ version: 1.2.2 ...@@ -283,10 +324,16 @@ version: 1.2.2
list.data('timepicker-input', self); list.data('timepicker-input', self);
self.data('timepicker-list', list); self.data('timepicker-list', list);
$('body').append(list); var appendTo = settings.appendTo;
if (typeof appendTo === 'string') {
appendTo = $(appendTo);
} else if (typeof appendTo === 'function') {
appendTo = appendTo(self);
}
appendTo.append(list);
_setSelected(self, list); _setSelected(self, list);
list.delegate('li', 'click', { 'timepicker': self }, function(e) { list.on('click', 'li', function(e) {
self.addClass('ui-timepicker-hideme'); self.addClass('ui-timepicker-hideme');
self[0].focus(); self[0].focus();
...@@ -297,7 +344,17 @@ version: 1.2.2 ...@@ -297,7 +344,17 @@ version: 1.2.2
_selectValue(self); _selectValue(self);
list.hide(); list.hide();
}); });
}; }
function _generateBaseDate()
{
var _baseDate = new Date();
var _currentTimezoneOffset = _baseDate.getTimezoneOffset()*60000;
_baseDate.setHours(0); _baseDate.setMinutes(0); _baseDate.setSeconds(0);
var _baseDateTimezoneOffset = _baseDate.getTimezoneOffset()*60000;
return new Date(_baseDate.valueOf() - _baseDateTimezoneOffset + _currentTimezoneOffset);
}
function _findRow(self, list, value) function _findRow(self, list, value)
{ {
...@@ -307,13 +364,16 @@ version: 1.2.2 ...@@ -307,13 +364,16 @@ version: 1.2.2
var settings = self.data('timepicker-settings'); var settings = self.data('timepicker-settings');
var out = false; var out = false;
var halfStep = settings.step*30;
// loop through the menu items // loop through the menu items
list.find('li').each(function(i, obj) { list.find('li').each(function(i, obj) {
var jObj = $(obj); var jObj = $(obj);
var offset = jObj.data('time') - value;
// check if the value is less than half a step from each row // check if the value is less than half a step from each row
if (Math.abs(jObj.data('time') - value) <= settings.step*30) { if (Math.abs(offset) < halfStep || offset == halfStep) {
out = jObj; out = jObj;
return false; return false;
} }
...@@ -333,12 +393,33 @@ version: 1.2.2 ...@@ -333,12 +393,33 @@ version: 1.2.2
function _formatValue() function _formatValue()
{ {
if (this.value == '') { if (this.value === '') {
return; return;
} }
var self = $(this); var self = $(this);
var prettyTime = _int2time(_time2int(this.value), self.data('timepicker-settings').timeFormat); var seconds = _time2int(this.value);
if (seconds === null) {
self.trigger('timeFormatError');
return;
}
var settings = self.data('timepicker-settings');
if (settings.forceRoundTime) {
var offset = seconds % (settings.step*60); // step is in minutes
if (offset >= settings.step*30) {
// if offset is larger than a half step, round up
seconds += (settings.step*60) - offset;
} else {
// round down
seconds -= offset;
}
}
var prettyTime = _int2time(seconds, settings.timeFormat);
self.val(prettyTime); self.val(prettyTime);
} }
...@@ -353,7 +434,7 @@ version: 1.2.2 ...@@ -353,7 +434,7 @@ version: 1.2.2
} else { } else {
return true; return true;
} }
}; }
switch (e.keyCode) { switch (e.keyCode) {
...@@ -362,13 +443,11 @@ version: 1.2.2 ...@@ -362,13 +443,11 @@ version: 1.2.2
methods.hide.apply(this); methods.hide.apply(this);
e.preventDefault(); e.preventDefault();
return false; return false;
break;
case 38: // up case 38: // up
var selected = list.find('.ui-timepicker-selected'); var selected = list.find('.ui-timepicker-selected');
if (!selected.length) { if (!selected.length) {
var selected;
list.children().each(function(i, obj) { list.children().each(function(i, obj) {
if ($(obj).position().top > 0) { if ($(obj).position().top > 0) {
selected = $(obj); selected = $(obj);
...@@ -389,10 +468,9 @@ version: 1.2.2 ...@@ -389,10 +468,9 @@ version: 1.2.2
break; break;
case 40: // down case 40: // down
var selected = list.find('.ui-timepicker-selected'); selected = list.find('.ui-timepicker-selected');
if (selected.length == 0) { if (selected.length === 0) {
var selected;
list.children().each(function(i, obj) { list.children().each(function(i, obj) {
if ($(obj).position().top > 0) { if ($(obj).position().top > 0) {
selected = $(obj); selected = $(obj);
...@@ -417,7 +495,10 @@ version: 1.2.2 ...@@ -417,7 +495,10 @@ version: 1.2.2
list.hide(); list.hide();
break; break;
case 9: case 9: //tab
methods.hide();
break;
case 16: case 16:
case 17: case 17:
case 18: case 18:
...@@ -436,11 +517,11 @@ version: 1.2.2 ...@@ -436,11 +517,11 @@ version: 1.2.2
list.find('li').removeClass('ui-timepicker-selected'); list.find('li').removeClass('ui-timepicker-selected');
return; return;
} }
}; }
function _selectValue(self) function _selectValue(self)
{ {
var settings = self.data('timepicker-settings') var settings = self.data('timepicker-settings');
var list = self.data('timepicker-list'); var list = self.data('timepicker-list');
var timeValue = null; var timeValue = null;
...@@ -448,12 +529,12 @@ version: 1.2.2 ...@@ -448,12 +529,12 @@ version: 1.2.2
if (cursor.length) { if (cursor.length) {
// selected value found // selected value found
var timeValue = cursor.data('time'); timeValue = cursor.data('time');
} else if (self.val()) { } else if (self.val()) {
// no selected value; fall back on input value // no selected value; fall back on input value
var timeValue = _time2int(self.val()); timeValue = _time2int(self.val());
_setSelected(self, list); _setSelected(self, list);
} }
...@@ -464,14 +545,14 @@ version: 1.2.2 ...@@ -464,14 +545,14 @@ version: 1.2.2
} }
self.trigger('change').trigger('changeTime'); self.trigger('change').trigger('changeTime');
}; }
function _int2duration(seconds) function _int2duration(seconds)
{ {
var minutes = Math.round(seconds/60); var minutes = Math.round(seconds/60);
var duration; var duration;
if (minutes < 60) { if (Math.abs(minutes) < 60) {
duration = [minutes, _lang.mins]; duration = [minutes, _lang.mins];
} else if (minutes == 60) { } else if (minutes == 60) {
duration = ['1', _lang.hr]; duration = ['1', _lang.hr];
...@@ -482,16 +563,21 @@ version: 1.2.2 ...@@ -482,16 +563,21 @@ version: 1.2.2
} }
return duration.join(' '); return duration.join(' ');
}; }
function _int2time(seconds, format) function _int2time(seconds, format)
{ {
if (seconds === null) {
return;
}
var time = new Date(_baseDate.valueOf() + (seconds*1000)); var time = new Date(_baseDate.valueOf() + (seconds*1000));
var output = ''; var output = '';
var hour, code;
for (var i=0; i<format.length; i++) { for (var i=0; i<format.length; i++) {
var code = format.charAt(i); code = format.charAt(i);
switch (code) { switch (code) {
case 'a': case 'a':
...@@ -503,8 +589,8 @@ version: 1.2.2 ...@@ -503,8 +589,8 @@ version: 1.2.2
break; break;
case 'g': case 'g':
var hour = time.getHours() % 12; hour = time.getHours() % 12;
output += (hour == 0) ? '12' : hour; output += (hour === 0) ? '12' : hour;
break; break;
case 'G': case 'G':
...@@ -512,17 +598,17 @@ version: 1.2.2 ...@@ -512,17 +598,17 @@ version: 1.2.2
break; break;
case 'h': case 'h':
var hour = time.getHours() % 12; hour = time.getHours() % 12;
if (hour != 0 && hour < 10) { if (hour !== 0 && hour < 10) {
hour = '0'+hour; hour = '0'+hour;
} }
output += (hour == 0) ? '12' : hour; output += (hour === 0) ? '12' : hour;
break; break;
case 'H': case 'H':
var hour = time.getHours(); hour = time.getHours();
output += (hour > 9) ? hour : '0'+hour; output += (hour > 9) ? hour : '0'+hour;
break; break;
...@@ -532,7 +618,7 @@ version: 1.2.2 ...@@ -532,7 +618,7 @@ version: 1.2.2
break; break;
case 's': case 's':
var seconds = time.getSeconds(); seconds = time.getSeconds();
output += (seconds > 9) ? seconds : '0'+seconds; output += (seconds > 9) ? seconds : '0'+seconds;
break; break;
...@@ -542,40 +628,42 @@ version: 1.2.2 ...@@ -542,40 +628,42 @@ version: 1.2.2
} }
return output; return output;
}; }
function _time2int(timeString) function _time2int(timeString)
{ {
if (timeString == '') return null; if (timeString === '') return null;
if (timeString+0 == timeString) return timeString; if (timeString+0 == timeString) return timeString;
if (typeof(timeString) == 'object') { if (typeof(timeString) == 'object') {
timeString = timeString.getHours()+':'+timeString.getMinutes(); timeString = timeString.getHours()+':'+timeString.getMinutes()+':'+timeString.getSeconds();
} }
var d = new Date(0); var d = new Date(0);
var time = timeString.toLowerCase().match(/(\d+)(?::(\d\d))?\s*([pa]?)/); var time = timeString.toLowerCase().match(/(\d{1,2})(?::(\d{1,2}))?(?::(\d{2}))?\s*([pa]?)/);
if (!time) { if (!time) {
return null; return null;
} }
var hour = parseInt(time[1]*1); var hour = parseInt(time[1]*1, 10);
var hours;
if (time[3]) { if (time[4]) {
if (hour == 12) { if (hour == 12) {
var hours = (time[3] == 'p') ? 12 : 0; hours = (time[4] == 'p') ? 12 : 0;
} else { } else {
var hours = (hour + (time[3] == 'p' ? 12 : 0)); hours = (hour + (time[4] == 'p' ? 12 : 0));
} }
} else { } else {
var hours = hour; hours = hour;
} }
var minutes = ( time[2]*1 || 0 ); var minutes = ( time[2]*1 || 0 );
return hours*3600 + minutes*60; var seconds = ( time[3]*1 || 0 );
}; return hours*3600 + minutes*60 + seconds;
}
// Plugin entry // Plugin entry
$.fn.timepicker = function(method) $.fn.timepicker = function(method)
...@@ -584,4 +672,4 @@ version: 1.2.2 ...@@ -584,4 +672,4 @@ version: 1.2.2
else if(typeof method === "object" || !method) { return methods.init.apply(this, arguments); } else if(typeof method === "object" || !method) { return methods.init.apply(this, arguments); }
else { $.error("Method "+ method + " does not exist on jQuery.timepicker"); } else { $.error("Method "+ method + " does not exist on jQuery.timepicker"); }
}; };
})(jQuery); }));
\ No newline at end of file
!function(e){function o(t){var r=t.data("timepicker-settings"),i=t.data("timepicker-list");i&&i.length&&(i.remove(),t.data("timepicker-list",!1)),i=e("<ul />"),i.attr("tabindex",-1),i.addClass("ui-timepicker-list"),r.className&&i.addClass(r.className),i.css({display:"none",position:"absolute"}),r.minTime!==null&&r.showDuration&&i.addClass("ui-timepicker-with-duration");var s=r.durationTime!==null?r.durationTime:r.minTime,o=r.minTime!==null?r.minTime:0,u=r.maxTime!==null?r.maxTime:o+n-1;u<=o&&(u+=n);for(var f=o;f<=u;f+=r.step*60){var l=f%n,d=e("<li />");d.data("time",l),d.text(p(l,r.timeFormat));if(r.minTime!==null&&r.showDuration){var v=e("<span />");v.addClass("ui-timepicker-duration"),v.text(" ("+h(f-s)+")"),d.append(v)}i.append(d)}i.data("timepicker-input",t),t.data("timepicker-list",i),e("body").append(i),a(t,i),i.delegate("li","click",{timepicker:t},function(n){t.addClass("ui-timepicker-hideme"),t[0].focus(),i.find("li").removeClass("ui-timepicker-selected"),e(this).addClass("ui-timepicker-selected"),c(t),i.hide()})}function u(t,n,r){if(!r&&r!==0)return!1;var i=t.data("timepicker-settings"),s=!1;return n.find("li").each(function(t,n){var o=e(n);if(Math.abs(o.data("time")-r)<=i.step*30)return s=o,!1}),s}function a(e,t){var n=d(e.val()),r=u(e,t,n);r&&r.addClass("ui-timepicker-selected")}function f(){if(this.value=="")return;var t=e(this),n=p(d(this.value),t.data("timepicker-settings").timeFormat);t.val(n)}function l(t){var n=e(this),r=n.data("timepicker-list");if(!r.is(":visible")){if(t.keyCode!=40)return!0;n.focus()}switch(t.keyCode){case 13:return c(n),s.hide.apply(this),t.preventDefault(),!1;case 38:var i=r.find(".ui-timepicker-selected");if(!i.length){var i;r.children().each(function(t,n){if(e(n).position().top>0)return i=e(n),!1}),i.addClass("ui-timepicker-selected")}else i.is(":first-child")||(i.removeClass("ui-timepicker-selected"),i.prev().addClass("ui-timepicker-selected"),i.prev().position().top<i.outerHeight()&&r.scrollTop(r.scrollTop()-i.outerHeight()));break;case 40:var i=r.find(".ui-timepicker-selected");if(i.length==0){var i;r.children().each(function(t,n){if(e(n).position().top>0)return i=e(n),!1}),i.addClass("ui-timepicker-selected")}else i.is(":last-child")||(i.removeClass("ui-timepicker-selected"),i.next().addClass("ui-timepicker-selected"),i.next().position().top+2*i.outerHeight()>r.outerHeight()&&r.scrollTop(r.scrollTop()+i.outerHeight()));break;case 27:r.find("li").removeClass("ui-timepicker-selected"),r.hide();break;case 9:case 16:case 17:case 18:case 19:case 20:case 33:case 34:case 35:case 36:case 37:case 39:case 45:return;default:r.find("li").removeClass("ui-timepicker-selected");return}}function c(e){var t=e.data("timepicker-settings"),n=e.data("timepicker-list"),r=null,i=n.find(".ui-timepicker-selected");if(i.length)var r=i.data("time");else if(e.val()){var r=d(e.val());a(e,n)}if(r!==null){var s=p(r,t.timeFormat);e.attr("value",s)}e.trigger("change").trigger("changeTime")}function h(e){var t=Math.round(e/60),n;if(t<60)n=[t,i.mins];else if(t==60)n=["1",i.hr];else{var r=(t/60).toFixed(1);i.decimal!="."&&(r=r.replace(".",i.decimal)),n=[r,i.hrs]}return n.join(" ")}function p(e,n){var r=new Date(t.valueOf()+e*1e3),i="";for(var s=0;s<n.length;s++){var o=n.charAt(s);switch(o){case"a":i+=r.getHours()>11?"pm":"am";break;case"A":i+=r.getHours()>11?"PM":"AM";break;case"g":var u=r.getHours()%12;i+=u==0?"12":u;break;case"G":i+=r.getHours();break;case"h":var u=r.getHours()%12;u!=0&&u<10&&(u="0"+u),i+=u==0?"12":u;break;case"H":var u=r.getHours();i+=u>9?u:"0"+u;break;case"i":var a=r.getMinutes();i+=a>9?a:"0"+a;break;case"s":var e=r.getSeconds();i+=e>9?e:"0"+e;break;default:i+=o}}return i}function d(e){if(e=="")return null;if(e+0==e)return e;typeof e=="object"&&(e=e.getHours()+":"+e.getMinutes());var t=new Date(0),n=e.toLowerCase().match(/(\d+)(?::(\d\d))?\s*([pa]?)/);if(!n)return null;var r=parseInt(n[1]*1);if(n[3])if(r==12)var i=n[3]=="p"?12:0;else var i=r+(n[3]=="p"?12:0);else var i=r;var s=n[2]*1||0;return i*3600+s*60}var t=new Date;t.setHours(0),t.setMinutes(0),t.setSeconds(0);var n=86400,r={className:null,minTime:null,maxTime:null,durationTime:null,step:30,showDuration:!1,timeFormat:"g:ia",scrollDefaultNow:!1,scrollDefaultTime:!1,selectOnBlur:!1},i={decimal:".",mins:"mins",hr:"hr",hrs:"hrs"},s={init:function(t){return this.each(function(){var n=e(this);if(n[0].tagName=="SELECT"){var o=e("<input />"),u={type:"text",value:n.val()},a=n[0].attributes;for(var c=0;c<a.length;c++)u[a[c].nodeName]=a[c].nodeValue;o.attr(u),n.replaceWith(o),n=o}var h=e.extend({},r);t&&(h=e.extend(h,t)),h.minTime&&(h.minTime=d(h.minTime)),h.maxTime&&(h.maxTime=d(h.maxTime)),h.durationTime&&(h.durationTime=d(h.durationTime)),h.lang&&(i=e.extend(i,h.lang)),n.data("timepicker-settings",h),n.attr("autocomplete","off"),n.click(s.show).focus(s.show).blur(f).keydown(l),n.addClass("ui-timepicker-input");if(n.val()){var v=p(d(n.val()),h.timeFormat);n.val(v)}e("body").attr("tabindex",-1).focusin(function(t){e(t.target).closest(".ui-timepicker-input").length==0&&e(t.target).closest(".ui-timepicker-list").length==0&&s.hide()})})},show:function(t){var n=e(this),r=n.data("timepicker-list");if(!r||r.length==0)o(n),r=n.data("timepicker-list");if(n.hasClass("ui-timepicker-hideme")){n.removeClass("ui-timepicker-hideme"),r.hide();return}if(r.is(":visible"))return;s.hide();var i=parseInt(n.css("marginTop").slice(0,-2));i||(i=0),n.offset().top+n.outerHeight(!0)+r.outerHeight()>e(window).height()+e(window).scrollTop()?r.css({left:n.offset().left,top:n.offset().top+i-r.outerHeight()}):r.css({left:n.offset().left,top:n.offset().top+i+n.outerHeight()}),r.show();var a=n.data("timepicker-settings"),f=r.find(".ui-timepicker-selected");f.length||(n.val()?f=u(n,r,d(n.val())):a.minTime===null&&a.scrollDefaultNow?f=u(n,r,d(new Date)):a.scrollDefaultTime!==!1&&(f=u(n,r,d(a.scrollDefaultTime))));if(f&&f.length){var l=r.scrollTop()+f.position().top-f.outerHeight();r.scrollTop(l)}else r.scrollTop(0);n.trigger("showTimepicker")},hide:function(t){e(".ui-timepicker-list:visible").each(function(){var t=e(this),n=t.data("timepicker-input"),r=n.data("timepicker-settings");r.selectOnBlur&&c(n),t.hide(),n.trigger("hideTimepicker")})},option:function(t,n){var r=e(this),i=r.data("timepicker-settings"),s=r.data("timepicker-list");if(typeof t=="object")i=e.extend(i,t);else if(typeof t=="string"&&typeof n!="undefined")i[t]=n;else if(typeof t=="string")return i[t];i.minTime&&(i.minTime=d(i.minTime)),i.maxTime&&(i.maxTime=d(i.maxTime)),i.durationTime&&(i.durationTime=d(i.durationTime)),r.data("timepicker-settings",i),s&&(s.remove(),r.data("timepicker-list",!1))},getSecondsFromMidnight:function(){return d(e(this).val())},getTime:function(){return new Date(t.valueOf()+d(e(this).val())*1e3)},setTime:function(t){var n=e(this),r=p(d(t),n.data("timepicker-settings").timeFormat);n.val(r)}};e.fn.timepicker=function(t){if(s[t])return s[t].apply(this,Array.prototype.slice.call(arguments,1));if(typeof t=="object"||!t)return s.init.apply(this,arguments);e.error("Method "+t+" does not exist on jQuery.timepicker")}}(jQuery) (function(e){typeof define=="function"&&define.amd?define(["jquery"],e):e(jQuery)})(function(e){function a(t){var r=t.data("timepicker-settings"),i=t.data("timepicker-list");i&&i.length&&(i.remove(),t.data("timepicker-list",!1)),i=e("<ul />"),i.attr("tabindex",-1),i.addClass("ui-timepicker-list"),r.className&&i.addClass(r.className),i.css({display:"none",position:"absolute"}),(r.minTime!==null||r.durationTime!==null)&&r.showDuration&&i.addClass("ui-timepicker-with-duration");var s=r.durationTime!==null?r.durationTime:r.minTime,o=r.minTime!==null?r.minTime:0,u=r.maxTime!==null?r.maxTime:o+n-1;u<=o&&(u+=n);for(var a=o;a<=u;a+=r.step*60){var f=a%n,l=e("<li />");l.data("time",f),l.text(m(f,r.timeFormat));if((r.minTime!==null||r.durationTime!==null)&&r.showDuration){var h=e("<span />");h.addClass("ui-timepicker-duration"),h.text(" ("+v(a-s)+")"),l.append(h)}i.append(l)}i.data("timepicker-input",t),t.data("timepicker-list",i);var p=r.appendTo;typeof p=="string"?p=e(p):typeof p=="function"&&(p=p(t)),p.append(i),c(t,i),i.on("click","li",function(n){t.addClass("ui-timepicker-hideme"),t[0].focus(),i.find("li").removeClass("ui-timepicker-selected"),e(this).addClass("ui-timepicker-selected"),d(t),i.hide()})}function f(){var e=new Date,t=e.getTimezoneOffset()*6e4;e.setHours(0),e.setMinutes(0),e.setSeconds(0);var n=e.getTimezoneOffset()*6e4;return new Date(e.valueOf()-n+t)}function l(t,n,r){if(!r&&r!==0)return!1;var i=t.data("timepicker-settings"),s=!1,o=i.step*30;return n.find("li").each(function(t,n){var i=e(n),u=i.data("time")-r;if(Math.abs(u)<o||u==o)return s=i,!1}),s}function c(e,t){var n=g(e.val()),r=l(e,t,n);r&&r.addClass("ui-timepicker-selected")}function h(){if(this.value==="")return;var t=e(this),n=g(this.value);if(n===null){t.trigger("timeFormatError");return}var r=t.data("timepicker-settings");if(r.forceRoundTime){var i=n%(r.step*60);i>=r.step*30?n+=r.step*60-i:n-=i}var s=m(n,r.timeFormat);t.val(s)}function p(t){var n=e(this),r=n.data("timepicker-list");if(!r.is(":visible")){if(t.keyCode!=40)return!0;n.focus()}switch(t.keyCode){case 13:return d(n),u.hide.apply(this),t.preventDefault(),!1;case 38:var i=r.find(".ui-timepicker-selected");i.length?i.is(":first-child")||(i.removeClass("ui-timepicker-selected"),i.prev().addClass("ui-timepicker-selected"),i.prev().position().top<i.outerHeight()&&r.scrollTop(r.scrollTop()-i.outerHeight())):(r.children().each(function(t,n){if(e(n).position().top>0)return i=e(n),!1}),i.addClass("ui-timepicker-selected"));break;case 40:i=r.find(".ui-timepicker-selected"),i.length===0?(r.children().each(function(t,n){if(e(n).position().top>0)return i=e(n),!1}),i.addClass("ui-timepicker-selected")):i.is(":last-child")||(i.removeClass("ui-timepicker-selected"),i.next().addClass("ui-timepicker-selected"),i.next().position().top+2*i.outerHeight()>r.outerHeight()&&r.scrollTop(r.scrollTop()+i.outerHeight()));break;case 27:r.find("li").removeClass("ui-timepicker-selected"),r.hide();break;case 9:u.hide();break;case 16:case 17:case 18:case 19:case 20:case 33:case 34:case 35:case 36:case 37:case 39:case 45:return;default:r.find("li").removeClass("ui-timepicker-selected");return}}function d(e){var t=e.data("timepicker-settings"),n=e.data("timepicker-list"),r=null,i=n.find(".ui-timepicker-selected");i.length?r=i.data("time"):e.val()&&(r=g(e.val()),c(e,n));if(r!==null){var s=m(r,t.timeFormat);e.attr("value",s)}e.trigger("change").trigger("changeTime")}function v(e){var t=Math.round(e/60),n;if(Math.abs(t)<60)n=[t,s.mins];else if(t==60)n=["1",s.hr];else{var r=(t/60).toFixed(1);s.decimal!="."&&(r=r.replace(".",s.decimal)),n=[r,s.hrs]}return n.join(" ")}function m(e,n){if(e===null)return;var r=new Date(t.valueOf()+e*1e3),i="",s,o;for(var u=0;u<n.length;u++){o=n.charAt(u);switch(o){case"a":i+=r.getHours()>11?"pm":"am";break;case"A":i+=r.getHours()>11?"PM":"AM";break;case"g":s=r.getHours()%12,i+=s===0?"12":s;break;case"G":i+=r.getHours();break;case"h":s=r.getHours()%12,s!==0&&s<10&&(s="0"+s),i+=s===0?"12":s;break;case"H":s=r.getHours(),i+=s>9?s:"0"+s;break;case"i":var a=r.getMinutes();i+=a>9?a:"0"+a;break;case"s":e=r.getSeconds(),i+=e>9?e:"0"+e;break;default:i+=o}}return i}function g(e){if(e==="")return null;if(e+0==e)return e;typeof e=="object"&&(e=e.getHours()+":"+e.getMinutes()+":"+e.getSeconds());var t=new Date(0),n=e.toLowerCase().match(/(\d{1,2})(?::(\d{1,2}))?(?::(\d{2}))?\s*([pa]?)/);if(!n)return null;var r=parseInt(n[1]*1,10),i;n[4]?r==12?i=n[4]=="p"?12:0:i=r+(n[4]=="p"?12:0):i=r;var s=n[2]*1||0,o=n[3]*1||0;return i*3600+s*60+o}var t=f(),n=86400,r="ontouchstart"in document?"touchstart":"mousedown",i={className:null,minTime:null,maxTime:null,durationTime:null,step:30,showDuration:!1,timeFormat:"g:ia",scrollDefaultNow:!1,scrollDefaultTime:!1,selectOnBlur:!1,forceRoundTime:!1,appendTo:"body"},s={decimal:".",mins:"mins",hr:"hr",hrs:"hrs"},o=!1,u={init:function(t){return this.each(function(){var n=e(this);if(n[0].tagName=="SELECT"){var a=e("<input />"),f={type:"text",value:n.val()},l=n[0].attributes;for(var c=0;c<l.length;c++)f[l[c].nodeName]=l[c].nodeValue;a.attr(f),n.replaceWith(a),n=a}var d=e.extend({},i);t&&(d=e.extend(d,t)),d.minTime&&(d.minTime=g(d.minTime)),d.maxTime&&(d.maxTime=g(d.maxTime)),d.durationTime&&(d.durationTime=g(d.durationTime)),d.lang&&(s=e.extend(s,d.lang)),n.data("timepicker-settings",d),n.attr("autocomplete","off"),n.on("click.timepicker focus.timepicker",u.show),n.on("blur.timepicker",h),n.on("keydown.timepicker",p),n.addClass("ui-timepicker-input"),h.call(n.get(0)),o||(e("body").on(r,function(t){var n=e(t.target),r=n.closest(".ui-timepicker-input");r.length===0&&n.closest(".ui-timepicker-list").length===0&&u.hide()}),o=!0)})},show:function(t){var n=e(this);"ontouchstart"in document&&n.blur();var r=n.data("timepicker-list");if(n.attr("readonly"))return;if(!r||r.length===0)a(n),r=n.data("timepicker-list");if(n.hasClass("ui-timepicker-hideme")){n.removeClass("ui-timepicker-hideme"),r.hide();return}if(r.is(":visible"))return;u.hide(),n.offset().top+n.outerHeight(!0)+r.outerHeight()>e(window).height()+e(window).scrollTop()?r.css({left:n.offset().left,top:n.offset().top-r.outerHeight()}):r.css({left:n.offset().left,top:n.offset().top+n.outerHeight()}),r.show();var i=n.data("timepicker-settings"),s=r.find(".ui-timepicker-selected");s.length||(n.val()?s=l(n,r,g(n.val())):i.scrollDefaultNow?s=l(n,r,g(new Date)):i.scrollDefaultTime!==!1&&(s=l(n,r,g(i.scrollDefaultTime))));if(s&&s.length){var o=r.scrollTop()+s.position().top-s.outerHeight();r.scrollTop(o)}else r.scrollTop(0);n.trigger("showTimepicker")},hide:function(t){e(".ui-timepicker-list:visible").each(function(){var t=e(this),n=t.data("timepicker-input"),r=n.data("timepicker-settings");r&&r.selectOnBlur&&d(n),t.hide(),n.trigger("hideTimepicker")})},option:function(t,n){var r=e(this),i=r.data("timepicker-settings"),s=r.data("timepicker-list");if(typeof t=="object")i=e.extend(i,t);else if(typeof t=="string"&&typeof n!="undefined")i[t]=n;else if(typeof t=="string")return i[t];i.minTime&&(i.minTime=g(i.minTime)),i.maxTime&&(i.maxTime=g(i.maxTime)),i.durationTime&&(i.durationTime=g(i.durationTime)),r.data("timepicker-settings",i),s&&(s.remove(),r.data("timepicker-list",!1))},getSecondsFromMidnight:function(){return g(e(this).val())},getTime:function(){return new Date(t.valueOf()+g(e(this).val())*1e3)},setTime:function(t){var n=e(this),r=m(g(t),n.data("timepicker-settings").timeFormat);n.val(r)},remove:function(){var t=e(this);if(!t.hasClass("ui-timepicker-input"))return;t.removeAttr("autocomplete","off"),t.removeClass("ui-timepicker-input"),t.removeData("timepicker-settings"),t.off(".timepicker"),t.data("timepicker-list")&&t.data("timepicker-list").remove(),t.removeData("timepicker-list")}};e.fn.timepicker=function(t){if(u[t])return u[t].apply(this,Array.prototype.slice.call(arguments,1));if(typeof t=="object"||!t)return u.init.apply(this,arguments);e.error("Method "+t+" does not exist on jQuery.timepicker")}});
\ No newline at end of file \ No newline at end of file
...@@ -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,14 +135,14 @@ def initialize_discussion_info(course): ...@@ -134,14 +135,14 @@ 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)
for module in all_modules:
skip_module = False skip_module = False
for key in ('id', 'discussion_category', 'for'): for key in ('id', 'discussion_category', 'for'):
if key not in module.metadata: if key not in module.metadata:
...@@ -157,7 +158,7 @@ def initialize_discussion_info(course): ...@@ -157,7 +158,7 @@ def initialize_discussion_info(course):
sort_key = module.metadata.get('sort_key', title) sort_key = module.metadata.get('sort_key', title)
category = " / ".join([x.strip() for x in category.split("/")]) category = " / ".join([x.strip() for x in category.split("/")])
last_category = category.split("/")[-1] last_category = category.split("/")[-1]
discussion_id_map[id] = {"location": location, "title": last_category + " / " + title} discussion_id_map[id] = {"location": module.location, "title": last_category + " / " + title}
unexpanded_category_map[category].append({"title": title, "id": id, unexpanded_category_map[category].append({"title": title, "id": id,
"sort_key": sort_key, "start_date": module.start}) "sort_key": sort_key, "start_date": module.start})
......
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