From 9dd6c56f9f07fe1d397d134ca58129598529d4c2 Mon Sep 17 00:00:00 2001 From: polesye Date: Mon, 7 Oct 2013 14:55:23 +0300 Subject: [PATCH 01/22] Add isoTime js view. --- cms/static/js/models/metadata.js | 1 + cms/static/js/views/metadata.js | 46 ++++++++++++++++++- cms/templates/base.html | 5 ++ .../js/vendor/jquery.maskedinput.min.js | 7 +++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 common/static/js/vendor/jquery.maskedinput.min.js diff --git a/cms/static/js/models/metadata.js b/cms/static/js/models/metadata.js index f4421b3079..cc63e5946e 100644 --- a/cms/static/js/models/metadata.js +++ b/cms/static/js/models/metadata.js @@ -108,6 +108,7 @@ define(["backbone"], function(Backbone) { Metadata.GENERIC_TYPE = "Generic"; Metadata.LIST_TYPE = "List"; Metadata.VIDEO_LIST_TYPE = "VideoList"; + Metadata.ISO_TIME_TYPE = "IsoTime"; return Metadata; }); diff --git a/cms/static/js/views/metadata.js b/cms/static/js/views/metadata.js index b555f7c048..970d45cfdf 100644 --- a/cms/static/js/views/metadata.js +++ b/cms/static/js/views/metadata.js @@ -2,7 +2,7 @@ define( [ "backbone", "underscore", "js/models/metadata", "js/views/abstract_editor", - "js/views/transcripts/metadata_videolist" + "js/views/transcripts/metadata_videolist", "jquery.maskedinput" ], function(Backbone, _, MetadataModel, AbstractEditor, VideoList) { var Metadata = {}; @@ -39,6 +39,8 @@ function(Backbone, _, MetadataModel, AbstractEditor, VideoList) { } else if(model.getType() === MetadataModel.VIDEO_LIST_TYPE) { new VideoList(data); + else if(model.getType() === MetadataModel.ISO_TIME_TYPE) { + new Metadata.IsoTime(data); } else { // Everything else is treated as GENERIC_TYPE, which uses String editor. @@ -292,5 +294,47 @@ function(Backbone, _, MetadataModel, AbstractEditor, VideoList) { } }); + Metadata.IsoTime = Metadata.AbstractEditor.extend({ + + events : { + "change input" : "updateModel", + "keypress .setting-input" : "showClearButton" , + "click .setting-clear" : "clear" + }, + + templateName: "metadata-string-entry", + + render: function () { + Metadata.AbstractEditor.prototype.render.apply(this); + + // Time format: HH:mm:ss + $.mask.definitions['h'] = '[0-2]'; + $.mask.definitions['H'] = '[0-3]'; + $.mask.definitions['m'] = $.mask.definitions['s'] = '[0-5]'; + $.mask.definitions['M'] = $.mask.definitions['S'] = '[0-9]'; + this.$el.find('#' + this.uniqueId).mask('hH:mM:sS', {placeholder: "0"}); + }, + + getValueFromEditor : function () { + var $input = this.$el.find('#' + this.uniqueId), + value = $input.val(), + time = Date.parse(value); + + if (time === null) { + value = null; + } + + return value; + }, + + setValueInEditor : function (value) { + if (!value) { + value = '00:00:00'; + } + + this.$el.find('input').val(value); + } + }); + return Metadata; }); diff --git a/cms/templates/base.html b/cms/templates/base.html index 541960911a..ded53b8188 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -52,6 +52,7 @@ var require = { "jquery.qtip": "js/vendor/jquery.qtip.min", "jquery.scrollTo": "js/vendor/jquery.scrollTo-1.4.2-min", "jquery.flot": "js/vendor/flot/jquery.flot.min", + "jquery.maskedinput": "js/vendor/jquery.maskedinput.min", "jquery.fileupload": "js/vendor/jQuery-File-Upload/js/jquery.fileupload", "jquery.iframe-transport": "js/vendor/jQuery-File-Upload/js/jquery.iframe-transport", "jquery.inputnumber": "js/vendor/html5-input-polyfills/number-polyfill", @@ -125,6 +126,10 @@ var require = { deps: ["jquery"], exports: "jQuery.fn.plot" }, + "jquery.maskedinput": { + deps: ["jquery"], + exports: "jQuery.fn.mask" + }, "jquery.fileupload": { deps: ["jquery.iframe-transport"], exports: "jQuery.fn.fileupload" diff --git a/common/static/js/vendor/jquery.maskedinput.min.js b/common/static/js/vendor/jquery.maskedinput.min.js new file mode 100644 index 0000000000..4769d6fda4 --- /dev/null +++ b/common/static/js/vendor/jquery.maskedinput.min.js @@ -0,0 +1,7 @@ +/* + Masked Input plugin for jQuery + Copyright (c) 2007-2013 Josh Bush (digitalbush.com) + Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) + Version: 1.3.1 +*/ +(function(e){function t(){var e=document.createElement("input"),t="onpaste";return e.setAttribute(t,""),"function"==typeof e[t]?"paste":"input"}var n,a=t()+".mask",r=navigator.userAgent,i=/iphone/i.test(r),o=/android/i.test(r);e.mask={definitions:{9:"[0-9]",a:"[A-Za-z]","*":"[A-Za-z0-9]"},dataName:"rawMaskFn",placeholder:"_"},e.fn.extend({caret:function(e,t){var n;if(0!==this.length&&!this.is(":hidden"))return"number"==typeof e?(t="number"==typeof t?t:e,this.each(function(){this.setSelectionRange?this.setSelectionRange(e,t):this.createTextRange&&(n=this.createTextRange(),n.collapse(!0),n.moveEnd("character",t),n.moveStart("character",e),n.select())})):(this[0].setSelectionRange?(e=this[0].selectionStart,t=this[0].selectionEnd):document.selection&&document.selection.createRange&&(n=document.selection.createRange(),e=0-n.duplicate().moveStart("character",-1e5),t=e+n.text.length),{begin:e,end:t})},unmask:function(){return this.trigger("unmask")},mask:function(t,r){var c,l,s,u,f,h;return!t&&this.length>0?(c=e(this[0]),c.data(e.mask.dataName)()):(r=e.extend({placeholder:e.mask.placeholder,completed:null},r),l=e.mask.definitions,s=[],u=h=t.length,f=null,e.each(t.split(""),function(e,t){"?"==t?(h--,u=e):l[t]?(s.push(RegExp(l[t])),null===f&&(f=s.length-1)):s.push(null)}),this.trigger("unmask").each(function(){function c(e){for(;h>++e&&!s[e];);return e}function d(e){for(;--e>=0&&!s[e];);return e}function m(e,t){var n,a;if(!(0>e)){for(n=e,a=c(t);h>n;n++)if(s[n]){if(!(h>a&&s[n].test(R[a])))break;R[n]=R[a],R[a]=r.placeholder,a=c(a)}b(),x.caret(Math.max(f,e))}}function p(e){var t,n,a,i;for(t=e,n=r.placeholder;h>t;t++)if(s[t]){if(a=c(t),i=R[t],R[t]=n,!(h>a&&s[a].test(i)))break;n=i}}function g(e){var t,n,a,r=e.which;8===r||46===r||i&&127===r?(t=x.caret(),n=t.begin,a=t.end,0===a-n&&(n=46!==r?d(n):a=c(n-1),a=46===r?c(a):a),k(n,a),m(n,a-1),e.preventDefault()):27==r&&(x.val(S),x.caret(0,y()),e.preventDefault())}function v(t){var n,a,i,l=t.which,u=x.caret();t.ctrlKey||t.altKey||t.metaKey||32>l||l&&(0!==u.end-u.begin&&(k(u.begin,u.end),m(u.begin,u.end-1)),n=c(u.begin-1),h>n&&(a=String.fromCharCode(l),s[n].test(a)&&(p(n),R[n]=a,b(),i=c(n),o?setTimeout(e.proxy(e.fn.caret,x,i),0):x.caret(i),r.completed&&i>=h&&r.completed.call(x))),t.preventDefault())}function k(e,t){var n;for(n=e;t>n&&h>n;n++)s[n]&&(R[n]=r.placeholder)}function b(){x.val(R.join(""))}function y(e){var t,n,a=x.val(),i=-1;for(t=0,pos=0;h>t;t++)if(s[t]){for(R[t]=r.placeholder;pos++a.length)break}else R[t]===a.charAt(pos)&&t!==u&&(pos++,i=t);return e?b():u>i+1?(x.val(""),k(0,h)):(b(),x.val(x.val().substring(0,i+1))),u?t:f}var x=e(this),R=e.map(t.split(""),function(e){return"?"!=e?l[e]?r.placeholder:e:void 0}),S=x.val();x.data(e.mask.dataName,function(){return e.map(R,function(e,t){return s[t]&&e!=r.placeholder?e:null}).join("")}),x.attr("readonly")||x.one("unmask",function(){x.unbind(".mask").removeData(e.mask.dataName)}).bind("focus.mask",function(){clearTimeout(n);var e;S=x.val(),e=y(),n=setTimeout(function(){b(),e==t.length?x.caret(0,e):x.caret(e)},10)}).bind("blur.mask",function(){y(),x.val()!=S&&x.change()}).bind("keydown.mask",g).bind("keypress.mask",v).bind(a,function(){setTimeout(function(){var e=y(!0);x.caret(e),r.completed&&e==x.val().length&&r.completed.call(x)},0)}),y()}))}})})(jQuery); From 6511dad131ca962183945f1aace28e0fa22a373f Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 1 Oct 2013 13:09:41 +0300 Subject: [PATCH 02/22] Video modul changes --- common/lib/xmodule/xmodule/video_module.py | 57 ++++++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index eccad071e9..6d26f748be 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -35,6 +35,43 @@ from xblock.runtime import DbModel log = logging.getLogger(__name__) +def parse_time_from_str_to_float(str_time): + """ + Converts s in '12:34:45' format to seconds. + + If s is None, returns 0""" + if not str_time: + return 0 + else: + obj_time = time.strptime(str_time, '%H:%M:%S') + return datetime.timedelta( + hours=obj_time.tm_hour, + minutes=obj_time.tm_min, + seconds=obj_time.tm_sec + ).total_seconds() + + +def parse_time_from_float_to_str(s): + """ + Converts s from seconds to '12:34:45' format. + + If s is None, returns "00:00:00" + """ + if not s: + return "00:00:00" + else: + return str(datetime.timedelta(seconds=s)) + + +class StringThatWasFloat(String): + + def from_json(self, value): + if isinstance(value, float): + return parse_time_from_float_to_str(value) + else: + return super(StringThatWasFloat, self).from_json(value) + + class VideoFields(object): """Fields for `VideoModule` and `VideoDescriptor`.""" display_name = String( @@ -79,17 +116,17 @@ class VideoFields(object): scope=Scope.settings, default="" ) - start_time = Float( + start_time = StringThatWasFloat( help="Start time for the video.", display_name="Start Time", scope=Scope.settings, - default=0.0 + default="00:00:00" ) - end_time = Float( + end_time = StringThatWasFloat( help="End time for the video.", display_name="End Time", scope=Scope.settings, - default=0.0 + default="00:00:00" ) source = String( help="The external URL to download the video. This appears as a link beneath the video.", @@ -182,8 +219,8 @@ class VideoModule(VideoFields, XModule): 'data_dir': getattr(self, 'data_dir', None), 'caption_asset_path': caption_asset_path, 'show_captions': json.dumps(self.show_captions), - 'start': self.start_time, - 'end': self.end_time, + 'start': parse_time_from_str_to_float(self.start_time), + 'end': parse_time_from_str_to_float(self.end_time), 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', False), # TODO: Later on the value 1500 should be taken from some global # configuration setting field. @@ -265,8 +302,8 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor attrs = { 'display_name': self.display_name, 'show_captions': json.dumps(self.show_captions), - 'start_time': datetime.timedelta(seconds=self.start_time), - 'end_time': datetime.timedelta(seconds=self.end_time), + 'start_time': self.start_time, + 'end_time': self.end_time, 'sub': self.sub, } for key, value in attrs.items(): @@ -360,8 +397,8 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor field_data = {} conversions = { - 'start_time': cls._parse_time, - 'end_time': cls._parse_time + # 'start_time': cls._parse_time, + # 'end_time': cls._parse_time } # Convert between key names for certain attributes -- From 7cbdac732223557e39fc54fa95b906ae2ff4025b Mon Sep 17 00:00:00 2001 From: polesye Date: Mon, 7 Oct 2013 16:15:05 +0300 Subject: [PATCH 03/22] Add new python field. --- common/lib/xmodule/xmodule/fields.py | 15 ++++++++++++++- common/lib/xmodule/xmodule/video_module.py | 18 +++++------------- common/lib/xmodule/xmodule/x_module.py | 3 +++ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index a0b53859ce..dfb6a4f484 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -2,7 +2,7 @@ import time import logging import re -from xblock.fields import Field +from xblock.fields import Field, String import datetime import dateutil.parser @@ -116,3 +116,16 @@ class Timedelta(Field): if cur_value > 0: values.append("%d %s" % (cur_value, attr)) return ' '.join(values) + + +class IsoTime(String): + + def from_json(self, value): + if isinstance(value, float): + if not value: + return "00:00:00" + else: + return str(datetime.timedelta(seconds=value)) + else: + return super(IsoTime, self).from_json(value) + diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 6d26f748be..99842ce197 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -28,13 +28,15 @@ from xmodule.editing_module import TabsEditingDescriptor from xmodule.raw_module import EmptyDataRawDescriptor from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field from xmodule.modulestore import Location -from xblock.fields import Scope, String, Boolean, Float, List, Integer, ScopeIds +from xblock.fields import Scope, String, Boolean, List, Integer, ScopeIds +from xmodule.fields import IsoTime from xmodule.modulestore.inheritance import InheritanceKeyValueStore from xblock.runtime import DbModel log = logging.getLogger(__name__) + def parse_time_from_str_to_float(str_time): """ Converts s in '12:34:45' format to seconds. @@ -62,16 +64,6 @@ def parse_time_from_float_to_str(s): else: return str(datetime.timedelta(seconds=s)) - -class StringThatWasFloat(String): - - def from_json(self, value): - if isinstance(value, float): - return parse_time_from_float_to_str(value) - else: - return super(StringThatWasFloat, self).from_json(value) - - class VideoFields(object): """Fields for `VideoModule` and `VideoDescriptor`.""" display_name = String( @@ -116,13 +108,13 @@ class VideoFields(object): scope=Scope.settings, default="" ) - start_time = StringThatWasFloat( + start_time = IsoTime( help="Start time for the video.", display_name="Start Time", scope=Scope.settings, default="00:00:00" ) - end_time = StringThatWasFloat( + end_time = IsoTime( help="End time for the video.", display_name="End Time", scope=Scope.settings, diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 7b5a2241a2..230ba88c2d 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -13,6 +13,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecif from xblock.core import XBlock from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String +from xmodule.fields import IsoTime from xblock.fragment import Fragment from xblock.runtime import Runtime from xmodule.errortracker import exc_info_to_str @@ -708,6 +709,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): editor_type = "Float" elif isinstance(field, List): editor_type = "List" + elif isinstance(field, IsoTime): + editor_type = "IsoTime" metadata_fields[field.name]['type'] = editor_type metadata_fields[field.name]['options'] = [] if values is None else values From 8e86d7b589d862f5b629f236a9d127c9d3030040 Mon Sep 17 00:00:00 2001 From: polesye Date: Fri, 18 Oct 2013 13:12:16 +0300 Subject: [PATCH 04/22] Refactor js. --- cms/static/js/views/metadata.js | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/cms/static/js/views/metadata.js b/cms/static/js/views/metadata.js index 970d45cfdf..4f55d82302 100644 --- a/cms/static/js/views/metadata.js +++ b/cms/static/js/views/metadata.js @@ -1,4 +1,3 @@ - define( [ "backbone", "underscore", "js/models/metadata", "js/views/abstract_editor", @@ -304,25 +303,31 @@ function(Backbone, _, MetadataModel, AbstractEditor, VideoList) { templateName: "metadata-string-entry", - render: function () { - Metadata.AbstractEditor.prototype.render.apply(this); + initialize: function () { + Metadata.AbstractEditor.prototype.initialize.apply(this); // Time format: HH:mm:ss - $.mask.definitions['h'] = '[0-2]'; - $.mask.definitions['H'] = '[0-3]'; - $.mask.definitions['m'] = $.mask.definitions['s'] = '[0-5]'; - $.mask.definitions['M'] = $.mask.definitions['S'] = '[0-9]'; - this.$el.find('#' + this.uniqueId).mask('hH:mM:sS', {placeholder: "0"}); + var definitions = { + h: '[0-2]', + H: '[0-3]', + m: '[0-5]', + s: '[0-5]', + M: '[0-9]', + S: '[0-9]' + }; + + $.each(definitions, function(key, value) { + $.mask.definitions[key] = value; + }); + + this.$el + .find('#' + this.uniqueId) + .mask('hH:mM:sS', { placeholder: '0' }); }, getValueFromEditor : function () { var $input = this.$el.find('#' + this.uniqueId), - value = $input.val(), - time = Date.parse(value); - - if (time === null) { - value = null; - } + value = $input.val(); return value; }, From ef3acaa7734dc53743325875913ce597226a98a1 Mon Sep 17 00:00:00 2001 From: polesye Date: Fri, 18 Oct 2013 14:04:38 +0300 Subject: [PATCH 05/22] Add js unit tests. --- cms/static/coffee/spec/main.coffee | 5 +++ .../spec/views/metadata_edit_spec.coffee | 44 ++++++++++++++++--- cms/static/js_test.yml | 1 + 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index de8a901e9d..bd349a97e3 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -18,6 +18,7 @@ requirejs.config({ "jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport", "jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill", "jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents", + "jquery.maskedinput": "xmodule_js/common_static/js/vendor/jquery.maskedinput.min", "datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair", "date": "xmodule_js/common_static/js/vendor/date", "underscore": "xmodule_js/common_static/js/vendor/underscore-min", @@ -94,6 +95,10 @@ requirejs.config({ deps: ["jquery"], exports: "jQuery.fn.inputNumber" }, + "jquery.maskedinput": { + deps: ["jquery"], + exports: "jQuery.fn.mask" + }, "jquery.tinymce": { deps: ["jquery", "tinymce"], exports: "jQuery.fn.tinymce" diff --git a/cms/static/coffee/spec/views/metadata_edit_spec.coffee b/cms/static/coffee/spec/views/metadata_edit_spec.coffee index 293cf0fadb..9ebf0dbf73 100644 --- a/cms/static/coffee/spec/views/metadata_edit_spec.coffee +++ b/cms/static/coffee/spec/views/metadata_edit_spec.coffee @@ -81,6 +81,18 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c value: ["the first display value", "the second"] } + timeEntry = { + default_value: "00:00:00", + display_name: "Time", + explicitly_set: true, + field_name: "iso_time", + help: "Specifies the name for this component.", + options: [], + type: MetadataModel.ISO_TIME_TYPE, + value: "12:12:12" + } + + # Test for the editor that creates the individual views. describe "MetadataView.Editor creates editors for each field", -> beforeEach -> @@ -103,17 +115,18 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c type: "unknown type", value: null }, - listEntry + listEntry, + timeEntry ] ) it "creates child views on initialize, and sorts them alphabetically", -> view = new MetadataView.Editor({collection: @model}) childModels = view.collection.models - expect(childModels.length).toBe(6) + expect(childModels.length).toBe(7) # Be sure to check list view as well as other input types childViews = view.$el.find('.setting-input, .list-settings') - expect(childViews.length).toBe(6) + expect(childViews.length).toBe(7) verifyEntry = (index, display_name, type) -> expect(childModels[index].get('display_name')).toBe(display_name) @@ -123,8 +136,9 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c verifyEntry(1, 'Inputs', 'number') verifyEntry(2, 'List', '') verifyEntry(3, 'Show Answer', 'select-one') - verifyEntry(4, 'Unknown', 'text') - verifyEntry(5, 'Weight', 'number') + verifyEntry(4, 'Time', 'text') + verifyEntry(5, 'Unknown', 'text') + verifyEntry(6, 'Weight', 'number') it "returns its display name", -> view = new MetadataView.Editor({collection: @model}) @@ -361,3 +375,23 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c @el.find('input').last().val('third setting') @el.find('input').last().trigger('input') expect(@el.find('.create-setting')).not.toHaveClass('is-disabled') + + describe "MetadataView.IsoTime allows the user to enter time string in HH:mm:ss format", -> + beforeEach -> + model = new MetadataModel(timeEntry) + @view = new MetadataView.IsoTime({model: model}) + + it "uses a text input type", -> + assertInputType(@view, 'text') + + it "returns the intial value upon initialization", -> + assertValueInView(@view, '12:12:12') + + it "can update its value in the view", -> + assertCanUpdateView(@view, "23:59:59") + + it "has a clear method to revert to the model default", -> + assertClear(@view, '00:00:00') + + it "has an update model method", -> + assertUpdateModel(@view, '12:12:12', '23:59:59') diff --git a/cms/static/js_test.yml b/cms/static/js_test.yml index 81c53e0f91..34c6966968 100644 --- a/cms/static/js_test.yml +++ b/cms/static/js_test.yml @@ -48,6 +48,7 @@ lib_paths: - xmodule_js/common_static/js/vendor/jasmine-jquery.js - xmodule_js/common_static/js/vendor/jasmine-stealth.js - xmodule_js/common_static/js/vendor/jasmine.async.js + - xmodule_js/common_static/js/vendor/jquery.maskedinput.min.js - xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js - xmodule_js/src/xmodule.js - xmodule_js/common_static/js/test/i18n.js From 28f229b7c92a6e72d97570441741cedf339cee14 Mon Sep 17 00:00:00 2001 From: polesye Date: Fri, 18 Oct 2013 14:12:21 +0300 Subject: [PATCH 06/22] Add comment. --- cms/static/js/views/metadata.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cms/static/js/views/metadata.js b/cms/static/js/views/metadata.js index 4f55d82302..dbfdda223f 100644 --- a/cms/static/js/views/metadata.js +++ b/cms/static/js/views/metadata.js @@ -306,7 +306,15 @@ function(Backbone, _, MetadataModel, AbstractEditor, VideoList) { initialize: function () { Metadata.AbstractEditor.prototype.initialize.apply(this); - // Time format: HH:mm:ss + // This list of definitions is used for creating appropriate + // time format mask; + // + // For example, mask 'hH:mM:sS': + // min value: 00:00:00 + // max value: 23:59:59 + // + // With this mask user cannot set following values: + // 93:23:23, 23:60:60, 77:77:77, etc. var definitions = { h: '[0-2]', H: '[0-3]', From 0da81e2a56884938d6d2b4694cfc1e96b709d106 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Fri, 18 Oct 2013 16:30:52 +0300 Subject: [PATCH 07/22] Update IsoTIme to be timedelta and update tests. --- common/lib/xmodule/xmodule/fields.py | 89 +++++++++++++++++-- .../lib/xmodule/xmodule/tests/test_video.py | 86 ++++++++++-------- common/lib/xmodule/xmodule/video_module.py | 65 +++----------- .../courseware/tests/test_video_xml.py | 16 ---- 4 files changed, 139 insertions(+), 117 deletions(-) diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index dfb6a4f484..e33bc02b0f 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -2,7 +2,7 @@ import time import logging import re -from xblock.fields import Field, String +from xblock.fields import Field import datetime import dateutil.parser @@ -118,14 +118,85 @@ class Timedelta(Field): return ' '.join(values) -class IsoTime(String): +class IsoTime(Field): + """ + Field for start_time and end_time video module properties. + + It was decided, that python representation of start_time and end_time + should be python datetime.timedelta object, to be consistent with + common time representation. + + At the same time, serialized representation should be"HH:MM:SS" + This format is convenient to use in XML (and it is used now), + and also it is used in frond-end studio editor of video module as format + for start and end time fields. + + In database we previously had float type for start_time and end_time fields, + so we are checking it also. + + Python object of IsoTime is datetime.timedelta. + JSONed representation of IsoTime is "HH:MM:SS" + """ + # Timedeltas are immutable, see http://docs.python.org/2/library/datetime.html#available-types + MUTABLE = False + + def _isotime_to_timedelta(self, value): + """ + Validate that value in "HH:MM:SS" format and convert to timedelta. + + Validate that user, that edits XML, sets proper format, and + that max value that can be used by user is "23:59:59". + """ + try: + obj_time = time.strptime(value, '%H:%M:%S') + except ValueError as e: + raise e( + "Incorrect IsoTime value {} was set in XML or serialized." + "Original parse message is {}".format(value, e.message) + ) + return datetime.timedelta( + hours=obj_time.tm_hour, + minutes=obj_time.tm_min, + seconds=obj_time.tm_sec + ) def from_json(self, value): - if isinstance(value, float): - if not value: - return "00:00:00" - else: - return str(datetime.timedelta(seconds=value)) - else: - return super(IsoTime, self).from_json(value) + """ + Convert value in 'HH:MM:SS' format to datetime.timedelta. + If not value, returns 0. + If value is float (backward compatibility issue), convert to timedelta. + """ + if not value: + return datetime.timedelta(seconds=0) + + # We've seen serialized versions of float in this field + if isinstance(value, float): + return datetime.timedelta(seconds=value) + + return self._isotime_to_timedelta(value) + + def to_json(self, value): + """ + Convert datetime.timedelta to "HH:MM:SS" format. + + If not value, return "00:00:00" + + Backward compatibility: check if value is float, and convert it. No exceptions here. + + If value is not float, but is exceed 23:59:59, raise exception. + """ + if not value: + return "00:00:00" + + if isinstance(value, float): # backward compatibility + if value > 86400: + value = 86400 + return str(datetime.timedelta(seconds=value)) + + if value.total_seconds() > 86400: # sanity check + raise ValueError( + "IsoTime max value is 23:59:59=86400 seconds" + "but {} seconds is passed".format(value.total_seconds()) + ) + return str(value) diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py index b6475c22e1..5e2f915ba4 100644 --- a/common/lib/xmodule/xmodule/tests/test_video.py +++ b/common/lib/xmodule/xmodule/tests/test_video.py @@ -14,6 +14,7 @@ the course, section, subsection, unit, etc. """ import unittest +import datetime from mock import Mock from . import LogicTest @@ -36,24 +37,6 @@ class VideoModuleTest(LogicTest): 'data': '