Commit ee02a2dd by Alexander Kryklia

Merge pull request #1421 from edx/anton/metadata-time-field

Anton/metadata time field
parents adb320c8 c351c3da
......@@ -42,10 +42,10 @@ def correct_video_settings(_step):
['Display Name', 'Video', False],
['Download Transcript', '', False],
['Download Video', '', False],
['End Time', '0', False],
['End Time', '00:00:00', False],
['HTML5 Transcript', '', False],
['Show Transcript', 'True', False],
['Start Time', '0', False],
['Start Time', '00:00:00', False],
['Video Sources', '', False],
['Youtube ID', 'OEoXaMPEzfM', False],
['Youtube ID for .75x speed', '', False],
......
......@@ -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"
......
......@@ -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: "relative_time",
help: "Specifies the name for this component.",
options: [],
type: MetadataModel.RELATIVE_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.RelativeTime allows the user to enter time string in HH:mm:ss format", ->
beforeEach ->
model = new MetadataModel(timeEntry)
@view = new MetadataView.RelativeTime({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')
......@@ -108,6 +108,7 @@ define(["backbone"], function(Backbone) {
Metadata.GENERIC_TYPE = "Generic";
Metadata.LIST_TYPE = "List";
Metadata.VIDEO_LIST_TYPE = "VideoList";
Metadata.RELATIVE_TIME_TYPE = "RelativeTime";
return Metadata;
});
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 = {};
......@@ -40,6 +39,9 @@ function(Backbone, _, MetadataModel, AbstractEditor, VideoList) {
else if(model.getType() === MetadataModel.VIDEO_LIST_TYPE) {
new VideoList(data);
}
else if(model.getType() === MetadataModel.RELATIVE_TIME_TYPE) {
new Metadata.RelativeTime(data);
}
else {
// Everything else is treated as GENERIC_TYPE, which uses String editor.
new Metadata.String(data);
......@@ -292,5 +294,61 @@ function(Backbone, _, MetadataModel, AbstractEditor, VideoList) {
}
});
Metadata.RelativeTime = AbstractEditor.extend({
events : {
"change input" : "updateModel",
"keypress .setting-input" : "showClearButton" ,
"click .setting-clear" : "clear"
},
templateName: "metadata-string-entry",
initialize: function () {
AbstractEditor.prototype.initialize.apply(this);
// 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]',
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();
return value;
},
setValueInEditor : function (value) {
if (!value) {
value = '00:00:00';
}
this.$el.find('input').val(value);
}
});
return Metadata;
});
......@@ -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
......
......@@ -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"
......
......@@ -116,3 +116,105 @@ class Timedelta(Field):
if cur_value > 0:
values.append("%d %s" % (cur_value, attr))
return ' '.join(values)
class RelativeTime(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 RelativeTime is datetime.timedelta.
JSONed representation of RelativeTime 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 ValueError(
"Incorrect RelativeTime value {!r} 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):
"""
Convert value is 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)
if isinstance(value, basestring):
return self._isotime_to_timedelta(value)
msg = "RelativeTime Field {0} has bad value '{1!r}'".format(self._name, value)
raise TypeError(msg)
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
value = min(value, 86400)
return self.timedelta_to_string(datetime.timedelta(seconds=value))
if isinstance(value, datetime.timedelta):
if value.total_seconds() > 86400: # sanity check
raise ValueError(
"RelativeTime max value is 23:59:59=86400.0 seconds, "
"but {} seconds is passed".format(value.total_seconds())
)
return self.timedelta_to_string(value)
raise TypeError("RelativeTime: cannot convert {!r} to json".format(value))
def timedelta_to_string(self, value):
"""
Makes first 'H' in str representation non-optional.
str(timedelta) has [H]H:MM:SS format, which is not suitable
for front-end (and ISO time standard), so we force HH:MM:SS format.
"""
stringified = str(value)
if len(stringified) == 7:
stringified = '0' + stringified
return stringified
......@@ -2,7 +2,7 @@
import datetime
import unittest
from django.utils.timezone import UTC
from xmodule.fields import Date, Timedelta
from xmodule.fields import Date, Timedelta, RelativeTime
from xmodule.timeinfo import TimeInfo
import time
......@@ -116,3 +116,59 @@ class TimeInfoTest(unittest.TestCase):
timeinfo = TimeInfo(due_date, grace_pd_string)
self.assertEqual(timeinfo.close_date,
due_date + Timedelta().from_json(grace_pd_string))
class RelativeTimeTest(unittest.TestCase):
delta = RelativeTime()
def test_from_json(self):
self.assertEqual(
RelativeTimeTest.delta.from_json('0:05:07'),
datetime.timedelta(seconds=307)
)
self.assertEqual(
RelativeTimeTest.delta.from_json(100.0),
datetime.timedelta(seconds=100)
)
self.assertEqual(
RelativeTimeTest.delta.from_json(None),
datetime.timedelta(seconds=0)
)
with self.assertRaises(TypeError):
RelativeTimeTest.delta.from_json(1234) # int
with self.assertRaises(ValueError):
RelativeTimeTest.delta.from_json("77:77:77")
def test_to_json(self):
self.assertEqual(
"01:02:03",
RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=3723))
)
self.assertEqual(
"00:00:00",
RelativeTimeTest.delta.to_json(None)
)
self.assertEqual(
"00:01:40",
RelativeTimeTest.delta.to_json(100.0)
)
with self.assertRaisesRegexp(ValueError, "RelativeTime max value is 23:59:59=86400.0 seconds, but 90000.0 seconds is passed"):
RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=90000))
with self.assertRaises(TypeError):
RelativeTimeTest.delta.to_json("123")
def test_str(self):
self.assertEqual(
"01:02:03",
RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=3723))
)
self.assertEqual(
"11:02:03",
RelativeTimeTest.delta.to_json(datetime.timedelta(seconds=39723))
)
......@@ -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': '<video />'
}
def test_parse_time_empty(self):
"""Ensure parse_time returns correctly with None or empty string."""
expected = ''
self.assertEqual(VideoDescriptor._parse_time(None), expected)
self.assertEqual(VideoDescriptor._parse_time(''), expected)
def test_parse_time(self):
"""Ensure that times are parsed correctly into seconds."""
expected = 247
output = VideoDescriptor._parse_time('00:04:07')
self.assertEqual(output, expected)
def test_parse_time_with_float(self):
"""Ensure that times are parsed correctly into seconds."""
expected = 247
output = VideoDescriptor._parse_time('247.0')
self.assertEqual(output, expected)
def test_parse_youtube(self):
"""Test parsing old-style Youtube ID strings into a dict."""
youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
......@@ -224,8 +207,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': '1EeWXzPdhSA',
'youtube_id_1_5': 'rABDYkeK0x8',
'show_captions': False,
'start_time': 1.0,
'end_time': 60,
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'html5_sources': ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'],
'data': ''
......@@ -250,8 +233,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': '1EeWXzPdhSA',
'youtube_id_1_5': 'rABDYkeK0x8',
'show_captions': False,
'start_time': 1.0,
'end_time': 60,
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'source': 'http://www.example.com/source.mp4',
'html5_sources': ['http://www.example.com/source.mp4'],
......@@ -279,8 +262,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': '1EeWXzPdhSA',
'youtube_id_1_5': '',
'show_captions': True,
'start_time': 0.0,
'end_time': 0.0,
'start_time': datetime.timedelta(seconds=0.0),
'end_time': datetime.timedelta(seconds=0.0),
'track': 'http://www.example.com/track',
'source': 'http://www.example.com/source.mp4',
'html5_sources': ['http://www.example.com/source.mp4'],
......@@ -300,8 +283,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': '',
'youtube_id_1_5': '',
'show_captions': True,
'start_time': 0.0,
'end_time': 0.0,
'start_time': datetime.timedelta(seconds=0.0),
'end_time': datetime.timedelta(seconds=0.0),
'track': '',
'source': '',
'html5_sources': [],
......@@ -334,8 +317,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': 'OEoXaMPEzf125',
'youtube_id_1_5': 'OEoXaMPEzf15',
'show_captions': False,
'start_time': 0.0,
'end_time': 0.0,
'start_time': datetime.timedelta(seconds=0.0),
'end_time': datetime.timedelta(seconds=0.0),
'track': 'http://download_track',
'source': 'http://download_video',
'html5_sources': ["source_1", "source_2"],
......@@ -356,8 +339,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': '1EeWXzPdhSA',
'youtube_id_1_5': '',
'show_captions': True,
'start_time': 0.0,
'end_time': 0.0,
'start_time': datetime.timedelta(seconds=0.0),
'end_time': datetime.timedelta(seconds=0.0),
'track': '',
'source': '',
'html5_sources': [],
......@@ -386,8 +369,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': '1EeWXzPdhSA',
'youtube_id_1_5': 'rABDYkeK0x8',
'show_captions': False,
'start_time': 1.0,
'end_time': 60,
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
......@@ -415,8 +398,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': '1EeWXzPdhSA',
'youtube_id_1_5': 'rABDYkeK0x8',
'show_captions': False,
'start_time': 1.0,
'end_time': 60,
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
......@@ -444,8 +427,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'youtube_id_1_25': '1EeWXzPdhSA',
'youtube_id_1_5': 'rABDYkeK0x8',
'show_captions': False,
'start_time': 1.0,
'end_time': 60.0,
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
......@@ -474,8 +457,8 @@ class VideoExportTestCase(unittest.TestCase):
desc.youtube_id_1_25 = '1EeWXzPdhSA'
desc.youtube_id_1_5 = 'rABDYkeK0x8'
desc.show_captions = False
desc.start_time = 1.0
desc.end_time = 60
desc.start_time = datetime.timedelta(seconds=1.0)
desc.end_time = datetime.timedelta(seconds=60)
desc.track = 'http://www.example.com/track'
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
......@@ -490,6 +473,33 @@ class VideoExportTestCase(unittest.TestCase):
self.assertXmlEqual(expected, xml)
def test_export_to_xml_empty_end_time(self):
"""Test that we write the correct XML on export."""
module_system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
desc = VideoDescriptor(module_system, DictFieldData({}), ScopeIds(None, None, location, location))
desc.youtube_id_0_75 = 'izygArpw-Qo'
desc.youtube_id_1_0 = 'p2Q6BrNhdh8'
desc.youtube_id_1_25 = '1EeWXzPdhSA'
desc.youtube_id_1_5 = 'rABDYkeK0x8'
desc.show_captions = False
desc.start_time = datetime.timedelta(seconds=5.0)
desc.end_time = datetime.timedelta(seconds=0.0)
desc.track = 'http://www.example.com/track'
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter
expected = etree.fromstring('''\
<video url_name="SampleProblem1" start_time="0:00:05" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false">
<source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/>
</video>
''')
self.assertXmlEqual(expected, xml)
def test_export_to_xml_empty_parameters(self):
"""Test XML export with defaults."""
module_system = DummySystem(load_error_modules=True)
......
......@@ -10,7 +10,7 @@ from xblock.field_data import DictFieldData
from xblock.fields import Scope, String, Dict, Boolean, Integer, Float, Any, List
from xblock.runtime import DbModel
from xmodule.fields import Date, Timedelta
from xmodule.fields import Date, Timedelta, RelativeTime
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritanceMixin
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
from xmodule.course_module import CourseDescriptor
......@@ -389,6 +389,28 @@ class TestDeserializeTimedelta(TestDeserialize):
self.assertDeserializeNonString()
class TestDeserializeRelativeTime(TestDeserialize):
""" Tests deserialize as related to Timedelta type. """
test_field = RelativeTime
def test_deserialize(self):
"""
There is no check for
self.assertDeserializeEqual('10:20:30', '10:20:30')
self.assertDeserializeNonString()
because these two tests work only because json.loads fires exception,
and xml_module.deserialized_field catches it and returns same value,
so there is nothing field-specific here.
But other modules do it, so I'm leaving this comment for PR reviewers.
"""
# test that from_json produces no exceptions
self.assertDeserializeEqual('10:20:30', '"10:20:30"')
class TestXmlAttributes(XModuleXmlImportTest):
def test_unknown_attribute(self):
......
......@@ -28,7 +28,8 @@ 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 RelativeTime
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
from xblock.runtime import DbModel
......@@ -79,18 +80,20 @@ class VideoFields(object):
scope=Scope.settings,
default=""
)
start_time = Float(
help="Start time for the video.",
start_time = RelativeTime( # datetime.timedelta object
help="Start time for the video (HH:MM:SS).",
display_name="Start Time",
scope=Scope.settings,
default=0.0
default=datetime.timedelta(seconds=0)
)
end_time = Float(
help="End time for the video.",
end_time = RelativeTime( # datetime.timedelta object
help="End time for the video (HH:MM:SS).",
display_name="End Time",
scope=Scope.settings,
default=0.0
default=datetime.timedelta(seconds=0)
)
#front-end code of video player checks logical validity of (start_time, end_time) pair.
source = String(
help="The external URL to download the video. This appears as a link beneath the video.",
display_name="Download Video",
......@@ -182,8 +185,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': self.start_time.total_seconds(),
'end': self.end_time.total_seconds(),
'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 +268,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():
......@@ -359,9 +362,10 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
xml = etree.fromstring(xml_data)
field_data = {}
# Convert between key types for certain attributes --
# necessary for backwards compatibility.
conversions = {
'start_time': cls._parse_time,
'end_time': cls._parse_time
# example: 'start_time': cls._example_convert_start_time
}
# Convert between key names for certain attributes --
......@@ -406,24 +410,6 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
return field_data
@classmethod
def _parse_time(cls, str_time):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if not str_time:
return ''
else:
try:
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()
except ValueError:
# We've seen serialized versions of float in this field
return float(str_time)
def _create_youtube_string(module):
"""
......
......@@ -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 RelativeTime
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, RelativeTime):
editor_type = "RelativeTime"
metadata_fields[field.name]['type'] = editor_type
metadata_fields[field.name]['options'] = [] if values is None else values
......
/*
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;)if(n=a.charAt(pos-1),s[t].test(n)){R[t]=n,i=t;break}if(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);
......@@ -15,7 +15,6 @@ common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the
course, section, subsection, unit, etc.
"""
import json
import unittest
from django.conf import settings
......@@ -109,21 +108,6 @@ class VideoModuleLogicTest(LogicTest):
'data': '<video />'
}
def test_parse_time(self):
"""Ensure that times are parsed correctly into seconds."""
output = VideoDescriptor._parse_time('00:04:07')
self.assertEqual(output, 247)
def test_parse_time_none(self):
"""Check parsing of None."""
output = VideoDescriptor._parse_time(None)
self.assertEqual(output, '')
def test_parse_time_empty(self):
"""Check parsing of the empty string."""
output = VideoDescriptor._parse_time('')
self.assertEqual(output, '')
def test_parse_youtube(self):
"""Test parsing old-style Youtube ID strings into a dict."""
youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment