Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
ee02a2dd
Commit
ee02a2dd
authored
Oct 24, 2013
by
Alexander Kryklia
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1421 from edx/anton/metadata-time-field
Anton/metadata time field
parents
adb320c8
c351c3da
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
370 additions
and
96 deletions
+370
-96
cms/djangoapps/contentstore/features/video-editor.py
+2
-2
cms/static/coffee/spec/main.coffee
+5
-0
cms/static/coffee/spec/views/metadata_edit_spec.coffee
+39
-5
cms/static/js/models/metadata.js
+1
-0
cms/static/js/views/metadata.js
+60
-2
cms/static/js_test.yml
+1
-0
cms/templates/base.html
+5
-0
common/lib/xmodule/xmodule/fields.py
+102
-0
common/lib/xmodule/xmodule/tests/test_fields.py
+57
-1
common/lib/xmodule/xmodule/tests/test_video.py
+48
-38
common/lib/xmodule/xmodule/tests/test_xml_module.py
+23
-1
common/lib/xmodule/xmodule/video_module.py
+17
-31
common/lib/xmodule/xmodule/x_module.py
+3
-0
common/static/js/vendor/jquery.maskedinput.min.js
+7
-0
lms/djangoapps/courseware/tests/test_video_xml.py
+0
-16
No files found.
cms/djangoapps/contentstore/features/video-editor.py
View file @
ee02a2dd
...
...
@@ -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'
,
'0
0:00:00
'
,
False
],
[
'HTML5 Transcript'
,
''
,
False
],
[
'Show Transcript'
,
'True'
,
False
],
[
'Start Time'
,
'0'
,
False
],
[
'Start Time'
,
'0
0:00:00
'
,
False
],
[
'Video Sources'
,
''
,
False
],
[
'Youtube ID'
,
'OEoXaMPEzfM'
,
False
],
[
'Youtube ID for .75x speed'
,
''
,
False
],
...
...
cms/static/coffee/spec/main.coffee
View file @
ee02a2dd
...
...
@@ -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"
...
...
cms/static/coffee/spec/views/metadata_edit_spec.coffee
View file @
ee02a2dd
...
...
@@ -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'
)
cms/static/js/models/metadata.js
View file @
ee02a2dd
...
...
@@ -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
;
});
cms/static/js/views/metadata.js
View file @
ee02a2dd
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
;
});
cms/static/js_test.yml
View file @
ee02a2dd
...
...
@@ -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
...
...
cms/templates/base.html
View file @
ee02a2dd
...
...
@@ -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"
...
...
common/lib/xmodule/xmodule/fields.py
View file @
ee02a2dd
...
...
@@ -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
common/lib/xmodule/xmodule/tests/test_fields.py
View file @
ee02a2dd
...
...
@@ -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
))
)
common/lib/xmodule/xmodule/tests/test_video.py
View file @
ee02a2dd
...
...
@@ -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
)
...
...
common/lib/xmodule/xmodule/tests/test_xml_module.py
View file @
ee02a2dd
...
...
@@ -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
):
...
...
common/lib/xmodule/xmodule/video_module.py
View file @
ee02a2dd
...
...
@@ -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
):
"""
...
...
common/lib/xmodule/xmodule/x_module.py
View file @
ee02a2dd
...
...
@@ -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
...
...
common/static/js/vendor/jquery.maskedinput.min.js
0 → 100644
View file @
ee02a2dd
/*
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"
,
-
1
e5
),
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
);
lms/djangoapps/courseware/tests/test_video_xml.py
View file @
ee02a2dd
...
...
@@ -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'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment