From c3aa86f1fb6ea69107eff750afd58650041a19e7 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 4 Oct 2012 14:53:43 -0400 Subject: [PATCH 1/4] remove template_dir_name from Vertical and Sequence descriptors so that it uses the default template dir --- common/lib/xmodule/xmodule/seq_module.py | 2 -- common/lib/xmodule/xmodule/vertical_module.py | 3 --- 2 files changed, 5 deletions(-) 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" From 758c4469291634893b50ab076027441a0546da46 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 5 Oct 2012 09:56:07 -0400 Subject: [PATCH 2/4] wip on setting date/times on subsection page --- cms/djangoapps/contentstore/utils.py | 28 +- cms/djangoapps/contentstore/views.py | 19 +- cms/static/js/base.js | 23 +- cms/templates/edit_subsection.html | 41 +- .../static/js/vendor/timepicker/datepair.js | 197 ++++++ .../vendor/timepicker/jquery.timepicker.css | 51 ++ .../js/vendor/timepicker/jquery.timepicker.js | 585 ++++++++++++++++++ .../timepicker/jquery.timepicker.min.js | 1 + 8 files changed, 914 insertions(+), 31 deletions(-) create mode 100644 common/static/js/vendor/timepicker/datepair.js create mode 100755 common/static/js/vendor/timepicker/jquery.timepicker.css create mode 100755 common/static/js/vendor/timepicker/jquery.timepicker.js create mode 100755 common/static/js/vendor/timepicker/jquery.timepicker.min.js 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..fe9204c21a 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) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 66f76c265f..3508c80aed 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 @@ -198,8 +203,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..b05c2121fc 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -1,9 +1,19 @@ <%inherit file="base.html" /> +<%! + import time + import dateutil.parser + from datetime import datetime + + now = datetime.now() +%> + <%! from django.core.urlresolvers import reverse %> <%block name="bodyclass">subsection <%block name="title">CMS Subsection <%namespace name="units" file="widgets/units.html" /> +<%namespace name='static' file='static_content.html'/> +<%namespace name='datetime' module='datetime'/> <%block name="content">
@@ -15,8 +25,8 @@
- - + +
@@ -35,31 +45,38 @@
-
- - +
+ +

The date above differs from the release date of Week 1 – 10/10/2012 at 12:00 am. Sync to Week 1.

Set a due date -
-

+

+

+ <% + due_date = dateutil.parser.parse(subsection.metadata.get('get')) if 'due' in subsection.metadata else None + %> + + Remove due date

-
- - hideshow -
+ +<%block name="jsextra"> + + + + diff --git a/common/static/js/vendor/timepicker/datepair.js b/common/static/js/vendor/timepicker/datepair.js new file mode 100644 index 0000000000..d547925e5b --- /dev/null +++ b/common/static/js/vendor/timepicker/datepair.js @@ -0,0 +1,197 @@ +$(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("
      "),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("
    • ");d.data("time",l),d.text(p(l,r.timeFormat));if(r.minTime!==null&&r.showDuration){var v=e("");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().top0)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;s11?"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(""),u={type:"text",value:n.val()},a=n[0].attributes;for(var c=0;ce(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) \ No newline at end of file From 95e15c4455ba7da281f87a0c1060344c80dc5d85 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 5 Oct 2012 13:17:51 -0400 Subject: [PATCH 3/4] time/date setting on Release Date and Due date --- cms/djangoapps/contentstore/views.py | 15 +++++++++++- cms/static/js/base.js | 34 +++++++++++++++++++++++++-- cms/templates/edit_subsection.html | 35 +++++++++++++++++++--------- cms/templates/widgets/units.html | 2 +- 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index fe9204c21a..df2a6fd986 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -440,15 +440,28 @@ 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) + + logging.debug(posted_metadata) + # 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(): + if posted_metadata[metadata_key] is None: + # remove both from passed in collection as well as the collection read in from the modulestore + 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 3508c80aed..bc637b0075 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -60,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(); @@ -70,10 +91,19 @@ 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; } + // OK, we have some metadata (namely 'Release Date' (aka 'start') and 'Due Date') which has been normalized in the UI + // we have to piece it back together. Unfortunate 'start' and 'due' use different string formatters. Rather than try to + // replicate the string formatting which is used in the backend here in JS, let's just pass back a unified format + // and let the server re-format into the expected persisted 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({ diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index b05c2121fc..3fa1e135dd 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -1,10 +1,9 @@ <%inherit file="base.html" /> <%! - import time + from time import mktime import dateutil.parser + import logging from datetime import datetime - - now = datetime.now() %> <%! from django.core.urlresolvers import reverse %> @@ -32,7 +31,7 @@ ${units.enum_units(subsection)} -
      +
      @@ -46,10 +45,13 @@
      - - + <% + 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.

      @@ -57,10 +59,11 @@

      <% - due_date = dateutil.parser.parse(subsection.metadata.get('get')) if 'due' in subsection.metadata else None - %> - - + # 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

      @@ -79,4 +82,14 @@ + + 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:
      From d55769861eb3dadf823784b0388b1f4239200732 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 5 Oct 2012 14:45:07 -0400 Subject: [PATCH 4/4] check if 'delete metadata field' is not in the list of system metadata --- cms/djangoapps/contentstore/views.py | 9 +++++---- cms/static/js/base.js | 7 +++---- common/static/js/vendor/timepicker/datepair.js | 12 ++++++++++++ .../static/js/vendor/timepicker/jquery.timepicker.js | 2 ++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index df2a6fd986..c1c3fc92d0 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -448,14 +448,15 @@ def save_item(request): # fetch original existing_item = modulestore().get_item(item_location) - logging.debug(posted_metadata) - # 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(): - if posted_metadata[metadata_key] is None: + # 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 - del existing_item.metadata[metadata_key] + 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 diff --git a/cms/static/js/base.js b/cms/static/js/base.js index bc637b0075..06e8f33c92 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -95,11 +95,10 @@ function saveSubsection(e) { metadata[$(el).data("metadata-name")] = el.value; } - // OK, we have some metadata (namely 'Release Date' (aka 'start') and 'Due Date') which has been normalized in the UI - // we have to piece it back together. Unfortunate 'start' and 'due' use different string formatters. Rather than try to - // replicate the string formatting which is used in the backend here in JS, let's just pass back a unified format - // and let the server re-format into the expected persisted format + // 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'); diff --git a/common/static/js/vendor/timepicker/datepair.js b/common/static/js/vendor/timepicker/datepair.js index d547925e5b..f210933593 100644 --- a/common/static/js/vendor/timepicker/datepair.js +++ b/common/static/js/vendor/timepicker/datepair.js @@ -1,3 +1,15 @@ +/************************ +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(){ diff --git a/common/static/js/vendor/timepicker/jquery.timepicker.js b/common/static/js/vendor/timepicker/jquery.timepicker.js index 506f66b5d1..3a462dd436 100755 --- a/common/static/js/vendor/timepicker/jquery.timepicker.js +++ b/common/static/js/vendor/timepicker/jquery.timepicker.js @@ -3,6 +3,8 @@ jquery-timepicker http://jonthornton.github.com/jquery-timepicker/ requires jQuery 1.6+ + +version: 1.2.2 ************************/