diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index fc801ac684..4000f011ba 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -1,13 +1,14 @@
+from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
-'''
-cdodge: for a given Xmodule, return the course that it belongs to
-NOTE: This makes a lot of assumptions about the format of the course location
-Also we have to assert that this module maps to only one course item - it'll throw an
-assert if not
-'''
def get_course_location_for_item(location):
+ '''
+ cdodge: for a given Xmodule, return the course that it belongs to
+ NOTE: This makes a lot of assumptions about the format of the course location
+ Also we have to assert that this module maps to only one course item - it'll throw an
+ assert if not
+ '''
item_loc = Location(location)
# check to see if item is already a course, if so we can skip this
@@ -29,3 +30,18 @@ def get_course_location_for_item(location):
location = courses[0].location
return location
+
+
+def get_lms_link_for_item(item):
+ if settings.LMS_BASE is not None:
+ lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format(
+ lms_base=settings.LMS_BASE,
+ # TODO: These will need to be changed to point to the particular instance of this problem in the particular course
+ course_id = modulestore().get_containing_courses(item.location)[0].id,
+ location=item.location,
+ )
+ else:
+ lms_link = None
+
+ return lms_link
+
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index b8da38f2c7..c1c3fc92d0 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -43,7 +43,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import ADMIN_ROLE_NAME, EDITOR_ROLE_NAME
-from .utils import get_course_location_for_item
+from .utils import get_course_location_for_item, get_lms_link_for_item
from xmodule.templates import all_templates
@@ -143,13 +143,18 @@ def edit_subsection(request, location):
item = modulestore().get_item(location)
+ lms_link = get_lms_link_for_item(item)
+
# make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential':
return HttpResponseBadRequest
+ logging.debug('Start = {0}'.format(item.start))
+
return render_to_response('edit_subsection.html',
{'subsection': item,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
+ 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
+ 'lms_link': lms_link
})
@login_required
@@ -167,15 +172,7 @@ def edit_unit(request, location):
item = modulestore().get_item(location)
- if settings.LMS_BASE is not None:
- lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format(
- lms_base=settings.LMS_BASE,
- # TODO: These will need to be changed to point to the particular instance of this problem in the particular course
- course_id = modulestore().get_containing_courses(item.location)[0].id,
- location=item.location,
- )
- else:
- lms_link = None
+ lms_link = get_lms_link_for_item(item)
component_templates = defaultdict(list)
@@ -443,15 +440,29 @@ def save_item(request):
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
- # note, that the postback is not the complete metadata, as there's system metadata which is
+ # NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST['metadata']:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location)
+
# update existing metadata with submitted metadata (which can be partial)
+ # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
+ for metadata_key in posted_metadata.keys():
+ # NOTE: We don't want clients to be able to delete 'system metadata' which are not intended to be user
+ # editable
+ if posted_metadata[metadata_key] is None and metadata_key not in existing_item.system_metadata_fields:
+ # remove both from passed in collection as well as the collection read in from the modulestore
+ if metadata_key in existing_item.metadata:
+ del existing_item.metadata[metadata_key]
+ del posted_metadata[metadata_key]
+
+ # overlay the new metadata over the modulestore sourced collection to support partial updates
existing_item.metadata.update(posted_metadata)
+
+ # commit to datastore
modulestore().update_metadata(item_location, existing_item.metadata)
return HttpResponse()
diff --git a/cms/static/js/base.js b/cms/static/js/base.js
index 66f76c265f..06e8f33c92 100644
--- a/cms/static/js/base.js
+++ b/cms/static/js/base.js
@@ -31,6 +31,11 @@ $(document).ready(function() {
$('.sortable-unit-list').sortable();
$('.sortable-unit-list').disableSelection();
$('.sortable-unit-list').bind('sortstop', onUnitReordered);
+
+ // expand/collapse methods for optional date setters
+ $('.set-date').bind('click', showDateSetter);
+ $('.remove-date').bind('click', removeDateSetter);
+
});
// This method only changes the ordering of the child objects in a subsection
@@ -55,6 +60,27 @@ function onUnitReordered() {
});
}
+function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
+ var input_date = $('#'+date_id).val();
+ var input_time = $('#'+time_id).val();
+
+ var edxTimeStr = null;
+
+ if (input_date != '') {
+ if (input_time == '')
+ input_time = '00:00';
+
+ // Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
+ date = Date.parse(input_date+" "+input_time);
+ if (format == null)
+ format = 'yyyy-MM-ddTHH:mm';
+
+ edxTimeStr = date.toString(format);
+ }
+
+ return edxTimeStr;
+}
+
function saveSubsection(e) {
e.preventDefault();
@@ -65,10 +91,18 @@ function saveSubsection(e) {
metadata = {};
for(var i=0; i< metadata_fields.length;i++) {
- el = metadata_fields[i];
- metadata[$(el).data("metadata-name")] = el.value;
+ el = metadata_fields[i];
+ metadata[$(el).data("metadata-name")] = el.value;
}
+
+ // Piece back together the date/time UI elements into one date/time string
+ // NOTE: our various "date/time" metadata elements don't always utilize the same formatting string
+ // so make sure we're passing back the correct format
+ metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time');
+ metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm');
+
+ // reordering is done through immediate callbacks when the resorting has completed in the UI
children =[];
$.ajax({
@@ -198,8 +232,22 @@ function hideHistoryModal(e) {
$modalCover.hide();
}
-
-
+function showDateSetter(e) {
+ e.preventDefault();
+ var $block = $(this).closest('.due-date-input');
+ $(this).hide();
+ $block.find('.date-setter').show();
+}
+
+function removeDateSetter(e) {
+ e.preventDefault();
+ var $block = $(this).closest('.due-date-input');
+ $block.find('.date-setter').hide();
+ $block.find('.set-date').show();
+ // clear out the values
+ $block.find('.date').val('');
+ $block.find('.time').val('');
+}
diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html
index 578e2a9ceb..3fa1e135dd 100644
--- a/cms/templates/edit_subsection.html
+++ b/cms/templates/edit_subsection.html
@@ -1,9 +1,18 @@
<%inherit file="base.html" />
+<%!
+ from time import mktime
+ import dateutil.parser
+ import logging
+ from datetime import datetime
+%>
+
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">subsection%block>
<%block name="title">CMS Subsection%block>
<%namespace name="units" file="widgets/units.html" />
+<%namespace name='static' file='static_content.html'/>
+<%namespace name='datetime' module='datetime'/>
<%block name="content">
@@ -15,14 +24,14 @@
-
-
+
+
${units.enum_units(subsection)}
-
+
@@ -35,31 +44,52 @@
-
-
-
+
+ <%
+ start_time = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
+ %>
+
+
-
The date above differs from the release date of Week 1 – 10/10/2012 at 12:00 am. Sync to Week 1.
+
The date above differs from the release date of Week 1 – 10/10/2012 at 12:00 am. Sync to Week 1.
+ <%
+ # due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
+ due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
+ %>
+
+
Remove due date
%block>
+
+<%block name="jsextra">
+
+
+
+
+
+%block>
diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html
index 8207b485a7..67e956561c 100644
--- a/cms/templates/widgets/units.html
+++ b/cms/templates/widgets/units.html
@@ -17,7 +17,7 @@ This def will enumerate through a passed in subsection and list all of the units
${unit.display_name}
- - private
+ - private
% if actions:
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index bcd2932537..0ade3e0e7d 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -118,8 +118,6 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
stores_state = True # For remembering where in the sequence the student is
- template_dir_name = 'sequence'
-
js = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]}
js_module_name = "SequenceDescriptor"
diff --git a/common/lib/xmodule/xmodule/vertical_module.py b/common/lib/xmodule/xmodule/vertical_module.py
index 738ee9ac5f..397bd3e136 100644
--- a/common/lib/xmodule/xmodule/vertical_module.py
+++ b/common/lib/xmodule/xmodule/vertical_module.py
@@ -45,9 +45,6 @@ class VerticalModule(XModule):
class VerticalDescriptor(SequenceDescriptor):
module_class = VerticalModule
- # cdodge: override the SequenceDescript's template_dir_name to point to default template directory
- template_dir_name = "default"
-
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
js_module_name = "VerticalDescriptor"
diff --git a/common/static/js/vendor/timepicker/datepair.js b/common/static/js/vendor/timepicker/datepair.js
new file mode 100644
index 0000000000..f210933593
--- /dev/null
+++ b/common/static/js/vendor/timepicker/datepair.js
@@ -0,0 +1,209 @@
+/************************
+datepair.js
+
+This is a component of the jquery-timepicker plugin
+
+http://jonthornton.github.com/jquery-timepicker/
+
+requires jQuery 1.6+
+
+version: 1.2.2
+************************/
+
+$(function() {
+
+ $('.datepair input.date').each(function(){
+ var $this = $(this);
+ $this.datepicker({ 'dateFormat': 'm/d/yy' });
+
+ if ($this.hasClass('start') || $this.hasClass('end')) {
+ $this.on('changeDate change', doDatepair);
+ }
+
+ });
+
+ $('.datepair input.time').each(function() {
+ var $this = $(this);
+ var opts = { 'showDuration': true, 'timeFormat': 'g:ia', 'scrollDefaultNow': true };
+
+ if ($this.hasClass('start') || $this.hasClass('end')) {
+ opts.onSelect = doDatepair;
+ }
+
+ $this.timepicker(opts);
+ });
+
+ $('.datepair').each(initDatepair);
+
+ function initDatepair()
+ {
+ var container = $(this);
+
+ var startDateInput = container.find('input.start.date');
+ var endDateInput = container.find('input.end.date');
+ var dateDelta = 0;
+
+ if (startDateInput.length && endDateInput.length) {
+ var startDate = new Date(startDateInput.val());
+ var endDate = new Date(endDateInput.val());
+
+ dateDelta = endDate.getTime() - startDate.getTime();
+
+ container.data('dateDelta', dateDelta);
+ }
+
+ var startTimeInput = container.find('input.start.time');
+ var endTimeInput = container.find('input.end.time');
+
+ if (startTimeInput.length && endTimeInput.length) {
+ var startInt = startTimeInput.timepicker('getSecondsFromMidnight');
+ var endInt = endTimeInput.timepicker('getSecondsFromMidnight');
+
+ container.data('timeDelta', endInt - startInt);
+
+ if (dateDelta < 86400000) {
+ endTimeInput.timepicker('option', 'minTime', startInt);
+ }
+ }
+ }
+
+ function doDatepair()
+ {
+ var target = $(this);
+ if (target.val() == '') {
+ return;
+ }
+
+ var container = target.closest('.datepair');
+
+ if (target.hasClass('date')) {
+ updateDatePair(target, container);
+
+ } else if (target.hasClass('time')) {
+ updateTimePair(target, container);
+ }
+ }
+
+ function updateDatePair(target, container)
+ {
+ var start = container.find('input.start.date');
+ var end = container.find('input.end.date');
+
+ if (!start.length || !end.length) {
+ return;
+ }
+
+ var startDate = new Date(start.val());
+ var endDate = new Date(end.val());
+
+ var oldDelta = container.data('dateDelta');
+
+ if (oldDelta && target.hasClass('start')) {
+ var newEnd = new Date(startDate.getTime()+oldDelta);
+ end.val(newEnd.format('m/d/Y'));
+ end.datepicker('update');
+ return;
+
+ } else {
+ var newDelta = endDate.getTime() - startDate.getTime();
+
+ if (newDelta < 0) {
+ newDelta = 0;
+
+ if (target.hasClass('start')) {
+ end.val(startDate.format('m/d/Y'));
+ end.datepicker('update');
+ } else if (target.hasClass('end')) {
+ start.val(endDate.format('m/d/Y'));
+ start.datepicker('update');
+ }
+ }
+
+ if (newDelta < 86400000) {
+ var startTimeVal = container.find('input.start.time').val();
+
+ if (startTimeVal) {
+ container.find('input.end.time').timepicker('option', {'minTime': startTimeVal});
+ }
+ } else {
+ container.find('input.end.time').timepicker('option', {'minTime': null});
+ }
+
+ container.data('dateDelta', newDelta);
+ }
+ }
+
+ function updateTimePair(target, container)
+ {
+ var start = container.find('input.start.time');
+ var end = container.find('input.end.time');
+
+ if (!start.length || !end.length) {
+ return;
+ }
+
+ var startInt = start.timepicker('getSecondsFromMidnight');
+ var endInt = end.timepicker('getSecondsFromMidnight');
+
+ var oldDelta = container.data('timeDelta');
+ var dateDelta = container.data('dateDelta');
+
+ if (target.hasClass('start') && (!dateDelta || dateDelta < 86400000)) {
+ end.timepicker('option', 'minTime', startInt);
+ }
+
+ var endDateAdvance = 0;
+ var newDelta;
+
+ if (oldDelta && target.hasClass('start')) {
+ // lock the duration and advance the end time
+
+ var newEnd = (startInt+oldDelta)%86400;
+
+ if (newEnd < 0) {
+ newEnd += 86400;
+ }
+
+ end.timepicker('setTime', newEnd);
+ newDelta = newEnd - startInt;
+ } else if (startInt !== null && endInt !== null) {
+ newDelta = endInt - startInt;
+ } else {
+ return;
+ }
+
+ container.data('timeDelta', newDelta);
+
+ if (newDelta < 0 && (!oldDelta || oldDelta > 0)) {
+ // overnight time span. advance the end date 1 day
+ var endDateAdvance = 86400000;
+
+ } else if (newDelta > 0 && oldDelta < 0) {
+ // switching from overnight to same-day time span. decrease the end date 1 day
+ var endDateAdvance = -86400000;
+ }
+
+ var startInput = container.find('.start.date');
+ var endInput = container.find('.end.date');
+
+ if (startInput.val() && !endInput.val()) {
+ endInput.val(startInput.val());
+ endInput.datepicker('update');
+ dateDelta = 0;
+ container.data('dateDelta', 0);
+ }
+
+ if (endDateAdvance != 0) {
+ if (dateDelta || dateDelta === 0) {
+ var endDate = new Date(endInput.val());
+ var newEnd = new Date(endDate.getTime() + endDateAdvance);
+ endInput.val(newEnd.format('m/d/Y'));
+ endInput.datepicker('update');
+ container.data('dateDelta', dateDelta + endDateAdvance);
+ }
+ }
+ }
+});
+
+// Simulates PHP's date function
+Date.prototype.format=function(format){var returnStr='';var replace=Date.replaceChars;for(var i=0;i');
+ var attrs = { 'type': 'text', 'value': self.val() };
+ var raw_attrs = self[0].attributes;
+
+ for (var i=0; i < raw_attrs.length; i++) {
+ attrs[raw_attrs[i].nodeName] = raw_attrs[i].nodeValue;
+ }
+
+ input.attr(attrs);
+ self.replaceWith(input);
+ self = input;
+ }
+
+ var settings = $.extend({}, _defaults);
+
+ if (options) {
+ settings = $.extend(settings, options);
+ }
+
+ if (settings.minTime) {
+ settings.minTime = _time2int(settings.minTime);
+ }
+
+ if (settings.maxTime) {
+ settings.maxTime = _time2int(settings.maxTime);
+ }
+
+ if (settings.durationTime) {
+ settings.durationTime = _time2int(settings.durationTime);
+ }
+
+ if (settings.lang) {
+ _lang = $.extend(_lang, settings.lang);
+ }
+
+ self.data('timepicker-settings', settings);
+ self.attr('autocomplete', 'off');
+ self.click(methods.show).focus(methods.show).blur(_formatValue).keydown(_keyhandler);
+ self.addClass('ui-timepicker-input');
+
+ if (self.val()) {
+ var prettyTime = _int2time(_time2int(self.val()), settings.timeFormat);
+ self.val(prettyTime);
+ }
+
+ // close the dropdown when container loses focus
+ $("body").attr("tabindex", -1).focusin(function(e) {
+ if ($(e.target).closest('.ui-timepicker-input').length == 0 && $(e.target).closest('.ui-timepicker-list').length == 0) {
+ methods.hide();
+ }
+ });
+
+ });
+ },
+
+ show: function(e)
+ {
+ var self = $(this);
+ var list = self.data('timepicker-list');
+
+ // check if list needs to be rendered
+ if (!list || list.length == 0) {
+ _render(self);
+ list = self.data('timepicker-list');
+ }
+
+ // check if a flag was set to close this picker
+ if (self.hasClass('ui-timepicker-hideme')) {
+ self.removeClass('ui-timepicker-hideme');
+ list.hide();
+ return;
+ }
+
+ if (list.is(':visible')) {
+ return;
+ }
+
+ // make sure other pickers are hidden
+ 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()) {
+ // position the dropdown on top
+ list.css({ 'left':(self.offset().left), 'top': self.offset().top + topMargin - list.outerHeight() });
+ } else {
+ // put it under the input
+ list.css({ 'left':(self.offset().left), 'top': self.offset().top + topMargin + self.outerHeight() });
+ }
+
+ list.show();
+
+ var settings = self.data('timepicker-settings');
+ // position scrolling
+ var selected = list.find('.ui-timepicker-selected');
+
+ if (!selected.length) {
+ if (self.val()) {
+ selected = _findRow(self, list, _time2int(self.val()));
+ } else if (settings.minTime === null && settings.scrollDefaultNow) {
+ selected = _findRow(self, list, _time2int(new Date()));
+ } else if (settings.scrollDefaultTime !== false) {
+ selected = _findRow(self, list, _time2int(settings.scrollDefaultTime));
+ }
+ }
+
+ if (selected && selected.length) {
+ var topOffset = list.scrollTop() + selected.position().top - selected.outerHeight();
+ list.scrollTop(topOffset);
+ } else {
+ list.scrollTop(0);
+ }
+
+ self.trigger('showTimepicker');
+ },
+
+ hide: function(e)
+ {
+ $('.ui-timepicker-list:visible').each(function() {
+ var list = $(this);
+ var self = list.data('timepicker-input');
+ var settings = self.data('timepicker-settings');
+ if (settings.selectOnBlur) {
+ _selectValue(self);
+ }
+
+ list.hide();
+ self.trigger('hideTimepicker');
+ });
+ },
+
+ option: function(key, value)
+ {
+ var self = $(this);
+ var settings = self.data('timepicker-settings');
+ var list = self.data('timepicker-list');
+
+ if (typeof key == 'object') {
+ settings = $.extend(settings, key);
+
+ } else if (typeof key == 'string' && typeof value != 'undefined') {
+ settings[key] = value;
+
+ } else if (typeof key == 'string') {
+ return settings[key];
+ }
+
+ if (settings.minTime) {
+ settings.minTime = _time2int(settings.minTime);
+ }
+
+ if (settings.maxTime) {
+ settings.maxTime = _time2int(settings.maxTime);
+ }
+
+ if (settings.durationTime) {
+ settings.durationTime = _time2int(settings.durationTime);
+ }
+
+ self.data('timepicker-settings', settings);
+
+ if (list) {
+ list.remove();
+ self.data('timepicker-list', false);
+ }
+
+ },
+
+ getSecondsFromMidnight: function()
+ {
+ return _time2int($(this).val());
+ },
+
+ getTime: function()
+ {
+ return new Date(_baseDate.valueOf() + (_time2int($(this).val())*1000));
+ },
+
+ setTime: function(value)
+ {
+ var self = $(this);
+ var prettyTime = _int2time(_time2int(value), self.data('timepicker-settings').timeFormat);
+ self.val(prettyTime);
+ }
+
+ };
+
+ // private methods
+
+ function _render(self)
+ {
+ var settings = self.data('timepicker-settings');
+ var list = self.data('timepicker-list');
+
+ if (list && list.length) {
+ list.remove();
+ self.data('timepicker-list', false);
+ }
+
+ list = $('
');
+ list.attr('tabindex', -1);
+ list.addClass('ui-timepicker-list');
+ if (settings.className) {
+ list.addClass(settings.className);
+ }
+
+ list.css({'display':'none', 'position': 'absolute' });
+
+ if (settings.minTime !== null && settings.showDuration) {
+ list.addClass('ui-timepicker-with-duration');
+ }
+
+ var durStart = (settings.durationTime !== null) ? settings.durationTime : settings.minTime;
+ var start = (settings.minTime !== null) ? settings.minTime : 0;
+ var end = (settings.maxTime !== null) ? settings.maxTime : (start + _ONE_DAY - 1);
+
+ if (end <= start) {
+ // make sure the end time is greater than start time, otherwise there will be no list to show
+ end += _ONE_DAY;
+ }
+
+ for (var i=start; i <= end; i += settings.step*60) {
+ var timeInt = i%_ONE_DAY;
+ var row = $('');
+ row.data('time', timeInt)
+ row.text(_int2time(timeInt, settings.timeFormat));
+
+ if (settings.minTime !== null && settings.showDuration) {
+ var duration = $('');
+ duration.addClass('ui-timepicker-duration');
+ duration.text(' ('+_int2duration(i - durStart)+')');
+ row.append(duration)
+ }
+
+ list.append(row);
+ }
+
+ list.data('timepicker-input', self);
+ self.data('timepicker-list', list);
+
+ $('body').append(list);
+ _setSelected(self, list);
+
+ list.delegate('li', 'click', { 'timepicker': self }, function(e) {
+ self.addClass('ui-timepicker-hideme');
+ self[0].focus();
+
+ // make sure only the clicked row is selected
+ list.find('li').removeClass('ui-timepicker-selected');
+ $(this).addClass('ui-timepicker-selected');
+
+ _selectValue(self);
+ list.hide();
+ });
+ };
+
+ function _findRow(self, list, value)
+ {
+ if (!value && value !== 0) {
+ return false;
+ }
+
+ var settings = self.data('timepicker-settings');
+ var out = false;
+
+ // loop through the menu items
+ list.find('li').each(function(i, obj) {
+ var jObj = $(obj);
+
+ // check if the value is less than half a step from each row
+ if (Math.abs(jObj.data('time') - value) <= settings.step*30) {
+ out = jObj;
+ return false;
+ }
+ });
+
+ return out;
+ }
+
+ function _setSelected(self, list)
+ {
+ var timeValue = _time2int(self.val());
+
+ var selected = _findRow(self, list, timeValue);
+ if (selected) selected.addClass('ui-timepicker-selected');
+ }
+
+
+ function _formatValue()
+ {
+ if (this.value == '') {
+ return;
+ }
+
+ var self = $(this);
+ var prettyTime = _int2time(_time2int(this.value), self.data('timepicker-settings').timeFormat);
+ self.val(prettyTime);
+ }
+
+ function _keyhandler(e)
+ {
+ var self = $(this);
+ var list = self.data('timepicker-list');
+
+ if (!list.is(':visible')) {
+ if (e.keyCode == 40) {
+ self.focus();
+ } else {
+ return true;
+ }
+ };
+
+ switch (e.keyCode) {
+
+ case 13: // return
+ _selectValue(self);
+ methods.hide.apply(this);
+ e.preventDefault();
+ return false;
+ break;
+
+ case 38: // up
+ var selected = list.find('.ui-timepicker-selected');
+
+ if (!selected.length) {
+ var selected;
+ list.children().each(function(i, obj) {
+ if ($(obj).position().top > 0) {
+ selected = $(obj);
+ return false;
+ }
+ });
+ selected.addClass('ui-timepicker-selected');
+
+ } else if (!selected.is(':first-child')) {
+ selected.removeClass('ui-timepicker-selected');
+ selected.prev().addClass('ui-timepicker-selected');
+
+ if (selected.prev().position().top < selected.outerHeight()) {
+ list.scrollTop(list.scrollTop() - selected.outerHeight());
+ }
+ }
+
+ break;
+
+ case 40: // down
+ var selected = list.find('.ui-timepicker-selected');
+
+ if (selected.length == 0) {
+ var selected;
+ list.children().each(function(i, obj) {
+ if ($(obj).position().top > 0) {
+ selected = $(obj);
+ return false;
+ }
+ });
+
+ selected.addClass('ui-timepicker-selected');
+ } else if (!selected.is(':last-child')) {
+ selected.removeClass('ui-timepicker-selected');
+ selected.next().addClass('ui-timepicker-selected');
+
+ if (selected.next().position().top + 2*selected.outerHeight() > list.outerHeight()) {
+ list.scrollTop(list.scrollTop() + selected.outerHeight());
+ }
+ }
+
+ break;
+
+ case 27: // escape
+ list.find('li').removeClass('ui-timepicker-selected');
+ list.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:
+ list.find('li').removeClass('ui-timepicker-selected');
+ return;
+ }
+ };
+
+ function _selectValue(self)
+ {
+ var settings = self.data('timepicker-settings')
+ var list = self.data('timepicker-list');
+ var timeValue = null;
+
+ var cursor = list.find('.ui-timepicker-selected');
+
+ if (cursor.length) {
+ // selected value found
+ var timeValue = cursor.data('time');
+
+ } else if (self.val()) {
+
+ // no selected value; fall back on input value
+ var timeValue = _time2int(self.val());
+
+ _setSelected(self, list);
+ }
+
+ if (timeValue !== null) {
+ var timeString = _int2time(timeValue, settings.timeFormat);
+ self.attr('value', timeString);
+ }
+
+ self.trigger('change').trigger('changeTime');
+ };
+
+ function _int2duration(seconds)
+ {
+ var minutes = Math.round(seconds/60);
+ var duration;
+
+ if (minutes < 60) {
+ duration = [minutes, _lang.mins];
+ } else if (minutes == 60) {
+ duration = ['1', _lang.hr];
+ } else {
+ var hours = (minutes/60).toFixed(1);
+ if (_lang.decimal != '.') hours = hours.replace('.', _lang.decimal);
+ duration = [hours, _lang.hrs];
+ }
+
+ return duration.join(' ');
+ };
+
+ function _int2time(seconds, format)
+ {
+ var time = new Date(_baseDate.valueOf() + (seconds*1000));
+ var output = '';
+
+ for (var i=0; i 11) ? 'pm' : 'am';
+ break;
+
+ case 'A':
+ output += (time.getHours() > 11) ? 'PM' : 'AM';
+ break;
+
+ case 'g':
+ var hour = time.getHours() % 12;
+ output += (hour == 0) ? '12' : hour;
+ break;
+
+ case 'G':
+ output += time.getHours();
+ break;
+
+ case 'h':
+ var hour = time.getHours() % 12;
+
+ if (hour != 0 && hour < 10) {
+ hour = '0'+hour;
+ }
+
+ output += (hour == 0) ? '12' : hour;
+ break;
+
+ case 'H':
+ var hour = time.getHours();
+ output += (hour > 9) ? hour : '0'+hour;
+ break;
+
+ case 'i':
+ var minutes = time.getMinutes();
+ output += (minutes > 9) ? minutes : '0'+minutes;
+ break;
+
+ case 's':
+ var seconds = time.getSeconds();
+ output += (seconds > 9) ? seconds : '0'+seconds;
+ break;
+
+ default:
+ output += code;
+ }
+ }
+
+ return output;
+ };
+
+ function _time2int(timeString)
+ {
+ if (timeString == '') return null;
+ if (timeString+0 == timeString) return timeString;
+
+ if (typeof(timeString) == 'object') {
+ timeString = timeString.getHours()+':'+timeString.getMinutes();
+ }
+
+ var d = new Date(0);
+ var time = timeString.toLowerCase().match(/(\d+)(?::(\d\d))?\s*([pa]?)/);
+
+ if (!time) {
+ return null;
+ }
+
+ var hour = parseInt(time[1]*1);
+
+ if (time[3]) {
+ if (hour == 12) {
+ var hours = (time[3] == 'p') ? 12 : 0;
+ } else {
+ var hours = (hour + (time[3] == 'p' ? 12 : 0));
+ }
+
+ } else {
+ var hours = hour;
+ }
+
+ var minutes = ( time[2]*1 || 0 );
+ return hours*3600 + minutes*60;
+ };
+
+ // Plugin entry
+ $.fn.timepicker = function(method)
+ {
+ if(methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); }
+ else if(typeof method === "object" || !method) { return methods.init.apply(this, arguments); }
+ else { $.error("Method "+ method + " does not exist on jQuery.timepicker"); }
+ };
+})(jQuery);
\ No newline at end of file
diff --git a/common/static/js/vendor/timepicker/jquery.timepicker.min.js b/common/static/js/vendor/timepicker/jquery.timepicker.min.js
new file mode 100755
index 0000000000..1678150ab1
--- /dev/null
+++ b/common/static/js/vendor/timepicker/jquery.timepicker.min.js
@@ -0,0 +1 @@
+!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("