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
0ebc9047
Commit
0ebc9047
authored
Aug 26, 2016
by
Chris Rodriguez
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
AC-571 updating video download/transcript area
parent
087acb8a
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
132 additions
and
635 deletions
+132
-635
common/lib/xmodule/xmodule/css/video/display.scss
+31
-33
common/lib/xmodule/xmodule/js/fixtures/video_all.html
+24
-14
common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js
+0
-290
common/lib/xmodule/xmodule/js/src/video/035_video_accessible_menu.js
+31
-203
common/test/acceptance/pages/lms/video/video.py
+9
-46
common/test/acceptance/pages/studio/video/video.py
+1
-1
common/test/acceptance/tests/video/test_video_module.py
+8
-17
lms/templates/video.html
+28
-31
No files found.
common/lib/xmodule/xmodule/css/video/display.scss
View file @
0ebc9047
...
...
@@ -21,7 +21,7 @@
.video
{
@include
clearfix
();
background
:
rgb
(
24
0
,
243
,
245
);
// UXPL grayscale-cool xx-light;
background
:
rgb
(
24
5
,
245
,
245
);
// UXPL grayscale x-back
display
:
block
;
margin
:
0
-12px
;
padding
:
12px
;
...
...
@@ -75,58 +75,56 @@
height
:
0px
;
}
.downloads-heading
{
margin
:
1em
0
0
0
;
}
.wrapper-downloads
{
display
:
flex
;
.hd
{
margin
:
0
;
padding
:
0
;
}
.video-download-button
{
display
:
inline-block
;
.wrapper-download-video
,
.wrapper-download-transcripts
,
.wrapper-handouts
,
.branding
{
flex
:
1
;
margin-top
:
$baseline
;
@include
padding-right
(
$baseline
);
vertical-align
:
top
;
margin
:
(
$baseline
*
0
.75
)
(
$baseline
/
2
)
0
0
;
>
a
{
@include
transition
(
all
$tmg-f2
ease-in-out
0s
);
@include
font-size
(
14
);
line-height
:
14px
;
float
:
left
;
border-radius
:
3px
;
background-color
:
$very-light-text
;
padding
:
(
$baseline
*
0
.75
);
color
:
$lighter-base-font-color
;
&
:hover
,
&
:focus
{
background-color
:
$action-primary-active-bg
;
color
:
$very-light-text
;
}
.wrapper-download-video
{
.video-sources
{
margin
:
0
;
}
}
.video-track
s
{
.wrapper-download-transcript
s
{
>
a
{
border-radius
:
3px
0
0
3px
;
}
.list-download-transcripts
{
margin
:
0
;
padding
:
0
;
list-style
:
none
;
>
a
.external-track
{
border-radius
:
3px
;
.transcript-option
{
margin
:
0
;
}
}
}
.branding
{
display
:
inline-block
;
float
:
right
;
margin
:
15px
0
0
10px
;
vertical-align
:
top
;
@include
padding-right
(
0
);
.host-tag
{
@include
margin-right
(
$baseline
/
2
);
position
:
absolute
;
left
:
-9999em
;
display
:
inline-block
;
vertical-align
:
middle
;
font-size
:
70%
;
color
:
#777
;
color
:
$base-font-color
;
}
.brand-logo
{
...
...
common/lib/xmodule/xmodule/js/fixtures/video_all.html
View file @
0ebc9047
...
...
@@ -10,8 +10,8 @@
<div
class=
"tc-wrapper"
>
<article
class=
"video-wrapper"
>
<span
tabindex=
"0"
class=
"spinner"
aria-hidden=
"false"
aria-label=
"
${_('Loading video player')}
"
></span>
<span
tabindex=
"-1"
class=
"btn-play is-hidden"
aria-hidden=
"true"
aria-label=
"
${_('Play video')}
"
></span>
<span
tabindex=
"0"
class=
"spinner"
aria-hidden=
"false"
aria-label=
"
Loading video player
"
></span>
<span
tabindex=
"-1"
class=
"btn-play is-hidden"
aria-hidden=
"true"
aria-label=
"
Play video
"
></span>
<div
class=
"video-player-pre"
></div>
<section
class=
"video-player"
>
<div
id=
"id"
></div>
...
...
@@ -30,21 +30,31 @@
<div
class=
"focus_grabber last"
></div>
<ul
class=
"wrapper-downloads"
>
<li
class=
"video-tracks"
>
<div
class=
"a11y-menu-container"
>
<a
class=
"a11y-menu-button"
href=
"#"
title=
".srt"
>
.srt
</a>
<ol
class=
"a11y-menu-list"
>
<li
class=
"a11y-menu-item"
>
<a
class=
"a11y-menu-item-link"
href=
"#txt"
title=
"Text (.txt) file"
data-value=
"txt"
>
Text (.txt) file
</a>
</li>
<li
class=
"a11y-menu-item active"
>
<a
class=
"a11y-menu-item-link"
href=
"#srt"
title=
"SubRip (.srt) file"
data-value=
"srt"
>
SubRip (.srt) file
</a>
</li>
</ol>
<h3
class=
"hd hd-4 downloads-heading sr"
id=
"video-download-transcripts"
>
Downloads and transcripts
</h3>
<div
class=
"wrapper-downloads"
role=
"region"
aria-labelledby=
"video-download-transcripts"
>
<div
class=
"wrapper-download-video"
>
<h4
class=
"hd hd-5"
>
Video
</h4>
<a
class=
"btn-link video-sources video-download-button"
href=
"#"
>
Download video file
</a>
</div>
<div
class=
"wrapper-download-transcripts"
>
<h4
class=
"hd hd-5"
>
Transcripts
</h4>
<ul
class=
"list-download-transcripts"
>
<li
class=
"transcript-option"
>
<a
href=
"#"
class=
"btn btn-link"
data-href=
"txt"
>
Download Text (.txt) file
</a>
</li>
<li
class=
"transcript-option"
>
<a
href=
"#"
class=
"btn btn-link"
data-href=
"srt"
>
Download SubRip (.srt) file
</a>
</li>
</ul>
<a
href=
"#"
class=
"external-track"
>
Download transcript
</a>
</div>
<div
class=
"wrapper-handouts"
>
<h4
class=
"hd hd-5"
>
Handouts
</h4>
<a
href=
"#"
>
Download Handout
</a>
</div>
</div>
</div>
</div>
...
...
common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js
deleted
100644 → 0
View file @
087acb8a
(
function
(
undefined
)
{
describe
(
'Video Accessible Menu'
,
function
()
{
var
state
;
afterEach
(
function
()
{
$
(
'source'
).
remove
();
state
.
storage
.
clear
();
state
.
videoPlayer
.
destroy
();
});
describe
(
'constructor'
,
function
()
{
describe
(
'always'
,
function
()
{
var
videoTracks
,
container
,
button
,
menu
,
menuItems
,
menuItemsLinks
;
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
videoTracks
=
$
(
'li.video-tracks'
);
container
=
videoTracks
.
children
(
'div.a11y-menu-container'
);
button
=
container
.
children
(
'a.a11y-menu-button'
);
menuList
=
container
.
children
(
'ol.a11y-menu-list'
);
menuItems
=
menuList
.
children
(
'li.a11y-menu-item'
);
menuItemsLinks
=
menuItems
.
children
(
'a.a11y-menu-item-link'
);
});
it
(
'add the accessible menu'
,
function
()
{
var
activeMenuItem
;
// Make sure we have the expected HTML structure:
// Menu container exists
expect
(
container
.
length
).
toBe
(
1
);
// Only one button and one menu list per menu container.
expect
(
button
.
length
).
toBe
(
1
);
expect
(
menuList
.
length
).
toBe
(
1
);
// At least one menu item and one menu link per menu
// container. Exact length test?
expect
(
menuItems
.
length
).
toBeGreaterThan
(
0
);
expect
(
menuItemsLinks
.
length
).
toBeGreaterThan
(
0
);
expect
(
menuItems
.
length
).
toBe
(
menuItemsLinks
.
length
);
// And one menu item is active
activeMenuItem
=
menuItems
.
filter
(
'.active'
);
expect
(
activeMenuItem
.
length
).
toBe
(
1
);
expect
(
activeMenuItem
.
children
(
'a.a11y-menu-item-link'
))
.
toHaveData
(
'value'
,
'srt'
);
expect
(
activeMenuItem
.
children
(
'a.a11y-menu-item-link'
))
.
toHaveHtml
(
'SubRip (.srt) file'
);
/* TO DO: Check that all the anchors contain correct text.
$.each(li.toArray().reverse(), function (index, link) {
expect($(link)).toHaveData(
'speed', state.videoSpeedControl.speeds[index]
);
expect($(link).find('a').text()).toBe(
state.videoSpeedControl.speeds[index] + 'x'
);
});
*/
});
});
describe
(
'when running'
,
function
()
{
var
videoTracks
,
container
,
button
,
menu
,
menuItems
,
menuItemsLinks
,
KEY
=
$
.
ui
.
keyCode
,
keyPressEvent
=
function
(
key
)
{
return
$
.
Event
(
'keydown'
,
{
keyCode
:
key
});
},
tabBackPressEvent
=
function
()
{
return
$
.
Event
(
'keydown'
,
{
keyCode
:
KEY
.
TAB
,
shiftKey
:
true
});
},
tabForwardPressEvent
=
function
()
{
return
$
.
Event
(
'keydown'
,
{
keyCode
:
KEY
.
TAB
,
shiftKey
:
false
});
},
// Get previous element in array or cyles back to the last
// if it is the first.
previousSpeed
=
function
(
index
)
{
return
speedEntries
.
eq
(
index
<
1
?
speedEntries
.
length
-
1
:
index
-
1
);
},
// Get next element in array or cyles back to the first if
// it is the last.
nextSpeed
=
function
(
index
)
{
return
speedEntries
.
eq
(
index
>=
speedEntries
.
length
-
1
?
0
:
index
+
1
);
};
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
videoTracks
=
$
(
'li.video-tracks'
);
container
=
videoTracks
.
children
(
'div.a11y-menu-container'
);
button
=
container
.
children
(
'a.a11y-menu-button'
);
menuList
=
container
.
children
(
'ol.a11y-menu-list'
);
menuItems
=
menuList
.
children
(
'li.a11y-menu-item'
);
menuItemsLinks
=
menuItems
.
children
(
'a.a11y-menu-item-link'
);
spyOn
(
$
.
fn
,
'focus'
).
and
.
callThrough
();
});
it
(
'open/close the menu on mouseenter/mouseleave'
,
function
()
{
container
.
mouseenter
();
expect
(
container
).
toHaveClass
(
'open'
);
container
.
mouseleave
();
expect
(
container
).
not
.
toHaveClass
(
'open'
);
});
it
(
'do not close the menu on mouseleave if a menu item has '
+
'focus'
,
function
()
{
// Open menu. Focus is on last menu item.
container
.
trigger
(
keyPressEvent
(
KEY
.
ENTER
));
container
.
mouseenter
().
mouseleave
();
expect
(
container
).
toHaveClass
(
'open'
);
});
it
(
'close the menu on click'
,
function
()
{
container
.
mouseenter
().
click
();
expect
(
container
).
not
.
toHaveClass
(
'open'
);
});
it
(
'close the menu on outside click'
,
function
()
{
container
.
trigger
(
keyPressEvent
(
KEY
.
ENTER
));
$
(
window
).
click
();
expect
(
container
).
not
.
toHaveClass
(
'open'
);
});
it
(
'open the menu on ENTER keydown'
,
function
()
{
container
.
trigger
(
keyPressEvent
(
KEY
.
ENTER
));
expect
(
container
).
toHaveClass
(
'open'
);
expect
(
menuItemsLinks
.
last
().
focus
).
toHaveBeenCalled
();
});
it
(
'open the menu on SPACE keydown'
,
function
()
{
container
.
trigger
(
keyPressEvent
(
KEY
.
SPACE
));
expect
(
container
).
toHaveClass
(
'open'
);
expect
(
menuItemsLinks
.
last
().
focus
).
toHaveBeenCalled
();
});
it
(
'open the menu on UP keydown'
,
function
()
{
container
.
trigger
(
keyPressEvent
(
KEY
.
UP
));
expect
(
container
).
toHaveClass
(
'open'
);
expect
(
menuItemsLinks
.
last
().
focus
).
toHaveBeenCalled
();
});
it
(
'close the menu on ESCAPE keydown'
,
function
()
{
container
.
trigger
(
keyPressEvent
(
KEY
.
ESCAPE
));
expect
(
container
).
not
.
toHaveClass
(
'open'
);
});
it
(
'UP and DOWN keydown function as expected on menu items'
,
function
()
{
// Iterate through list in both directions and check if
// things wrap up correctly.
var
lastEntry
=
menuItemsLinks
.
length
-
1
,
i
;
// First open menu
container
.
trigger
(
keyPressEvent
(
KEY
.
UP
));
// Iterate with UP key until we have looped.
for
(
i
=
lastEntry
;
i
>=
0
;
i
--
)
{
menuItemsLinks
.
eq
(
i
).
trigger
(
keyPressEvent
(
KEY
.
UP
));
}
// Iterate with DOWN key until we have looped.
for
(
i
=
0
;
i
<=
lastEntry
;
i
++
)
{
menuItemsLinks
.
eq
(
i
).
trigger
(
keyPressEvent
(
KEY
.
DOWN
));
}
// Test if each element has been called twice.
expect
(
$
.
fn
.
focus
.
calls
.
count
())
.
toEqual
(
2
*
menuItemsLinks
.
length
+
1
);
});
it
(
'ESC keydown on menu item closes menu'
,
function
()
{
// First open menu. Focus is on last speed entry.
container
.
trigger
(
keyPressEvent
(
KEY
.
UP
));
menuItemsLinks
.
last
().
trigger
(
keyPressEvent
(
KEY
.
ESCAPE
));
// Menu is closed and focus has been returned to speed
// control.
expect
(
container
).
not
.
toHaveClass
(
'open'
);
expect
(
container
.
focus
).
toHaveBeenCalled
();
});
it
(
'ENTER keydown on menu item selects its data and closes menu'
,
function
()
{
// First open menu.
container
.
trigger
(
keyPressEvent
(
KEY
.
UP
));
// Focus on '.txt'
menuItemsLinks
.
eq
(
0
).
focus
();
menuItemsLinks
.
eq
(
0
).
trigger
(
keyPressEvent
(
KEY
.
ENTER
));
// Menu is closed, focus has been returned to container
// and file format is '.txt'.
/* TO DO
expect(container.focus).toHaveBeenCalled();
expect($('.video_speeds li[data-speed="1.50"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('1.50x');
*/
});
it
(
'SPACE keydown on menu item selects its data and closes menu'
,
function
()
{
// First open menu.
container
.
trigger
(
keyPressEvent
(
KEY
.
UP
));
// Focus on '.txt'
menuItemsLinks
.
eq
(
0
).
focus
();
menuItemsLinks
.
eq
(
0
).
trigger
(
keyPressEvent
(
KEY
.
SPACE
));
// Menu is closed, focus has been returned to container
// and file format is '.txt'.
/* TO DO
expect(speedControl.focus).toHaveBeenCalled();
expect($('.video_speeds li[data-speed="1.50"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('1.50x');
*/
});
// TO DO? No such behavior implemented.
xit
(
'TAB + SHIFT keydown on speed entry closes menu and gives '
+
'focus to Play/Pause control'
,
function
()
{
// First open menu. Focus is on last speed entry.
speedControl
.
trigger
(
keyPressEvent
(
KEY
.
UP
));
speedEntries
.
last
().
trigger
(
tabBackPressEvent
());
// Menu is closed and focus has been given to Play/Pause
// control.
expect
(
state
.
videoControl
.
playPauseEl
.
focus
)
.
toHaveBeenCalled
();
});
// TO DO? No such behavior implemented.
xit
(
'TAB keydown on speed entry closes menu and gives focus '
+
'to Volume control'
,
function
()
{
// First open menu. Focus is on last speed entry.
speedControl
.
trigger
(
keyPressEvent
(
KEY
.
UP
));
speedEntries
.
last
().
trigger
(
tabForwardPressEvent
());
// Menu is closed and focus has been given to Volume
// control.
expect
(
state
.
videoVolumeControl
.
buttonEl
.
focus
)
.
toHaveBeenCalled
();
});
});
});
// TODO
xdescribe
(
'change file format'
,
function
()
{
describe
(
'when new file format is not the same'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
state
.
videoSpeedControl
.
setSpeed
(
1.0
);
spyOn
(
state
.
videoPlayer
,
'onSpeedChange'
).
and
.
callThrough
();
$
(
'li[data-speed="0.75"] .speed-link'
).
click
();
});
it
(
'trigger speedChange event'
,
function
()
{
expect
(
state
.
videoPlayer
.
onSpeedChange
).
toHaveBeenCalled
();
expect
(
state
.
videoSpeedControl
.
currentSpeed
).
toEqual
(
0.75
);
});
});
});
// TODO
xdescribe
(
'onSpeedChange'
,
function
()
{
beforeEach
(
function
()
{
state
=
jasmine
.
initializePlayer
();
$
(
'li[data-speed="1.0"] .speed-link'
).
addClass
(
'active'
);
state
.
videoSpeedControl
.
setSpeed
(
0.75
);
});
it
(
'set the new speed as active'
,
function
()
{
expect
(
$
(
'.video_speeds li[data-speed="1.0"]'
))
.
not
.
toHaveClass
(
'active'
);
expect
(
$
(
'.video_speeds li[data-speed="0.75"]'
))
.
toHaveClass
(
'active'
);
expect
(
$
(
'.speeds p.active'
)).
toHaveHtml
(
'0.75x'
);
});
});
});
}).
call
(
this
);
common/lib/xmodule/xmodule/js/src/video/035_video_accessible_menu.js
View file @
0ebc9047
(
function
(
define
)
{
'use strict'
;
// Video
AccessibleMenu
module.
// Video
TranscriptDownloadHandler
module.
define
(
'video/035_video_accessible_menu.js'
,
[],
function
()
{
'video/035_video_accessible_menu.js'
,
[
'underscore'
],
function
(
_
)
{
/**
* Video Download Transcript control module.
* @exports video/035_video_accessible_menu.js
...
...
@@ -11,231 +11,59 @@ function() {
* @param {jquery Element} element
* @param {Object} options
*/
var
Video
AccessibleMenu
=
function
(
element
,
options
)
{
if
(
!
(
this
instanceof
Video
AccessibleMenu
))
{
return
new
Video
AccessibleMenu
(
element
,
options
);
var
Video
TranscriptDownloadHandler
=
function
(
element
,
options
)
{
if
(
!
(
this
instanceof
Video
TranscriptDownloadHandler
))
{
return
new
Video
TranscriptDownloadHandler
(
element
,
options
);
}
_
.
bindAll
(
this
,
'openMenu'
,
'openMenuHandler'
,
'closeMenu'
,
'closeMenuHandler'
,
'toggleMenuHandler'
,
'clickHandler'
,
'keyDownHandler'
,
'render'
,
'menuItemsLinksFocused'
,
'changeFileType'
,
'setValue'
);
_
.
bindAll
(
this
,
'clickHandler'
);
this
.
container
=
element
;
this
.
options
=
options
||
{};
if
(
this
.
container
.
find
(
'.
video-track
s'
))
{
if
(
this
.
container
.
find
(
'.
wrapper-downloads .wrapper-download-transcript
s'
))
{
this
.
initialize
();
}
return
false
;
};
Video
AccessibleMenu
.
prototype
=
{
/
** Initializes the module. */
Video
TranscriptDownloadHandler
.
prototype
=
{
/
/ Initializes the module.
initialize
:
function
()
{
this
.
value
=
this
.
options
.
storage
.
getItem
(
'transcript_download_format'
);
this
.
el
=
this
.
container
.
find
(
'.video-tracks .a11y-menu-container'
);
this
.
render
();
this
.
bindHandlers
();
},
/**
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
*/
render
:
function
()
{
var
value
,
msg
;
// For the time being, we assume that the menu structure is present in
// the template HTML. In the future accessible menu plugin, everything
// inside <div class='menu-container'></div> will be generated in this
// file.
this
.
button
=
this
.
el
.
children
(
'.a11y-menu-button'
);
this
.
menuList
=
this
.
el
.
children
(
'.a11y-menu-list'
);
this
.
menuItems
=
this
.
menuList
.
children
(
'.a11y-menu-item'
);
this
.
menuItemsLinks
=
this
.
menuItems
.
children
(
'.a11y-menu-item-link'
);
value
=
(
function
(
val
,
activeElement
)
{
return
val
||
activeElement
.
find
(
'a'
).
data
(
'value'
)
||
'srt'
;
}(
this
.
value
,
this
.
menuItems
.
filter
(
'.active'
)));
msg
=
'.'
+
value
;
if
(
value
)
{
this
.
setValue
(
value
);
this
.
button
.
text
(
gettext
(
msg
));
}
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers
:
function
()
{
// Attach various events handlers to menu container.
this
.
el
.
on
({
'mouseenter'
:
this
.
openMenuHandler
,
'mouseleave'
:
this
.
closeMenuHandler
,
'click'
:
this
.
toggleMenuHandler
,
'keydown'
:
this
.
keyDownHandler
});
// Attach click and keydown event handlers to individual menu items.
this
.
menuItems
.
on
(
'click'
,
'a.a11y-menu-item-link'
,
this
.
clickHandler
)
.
on
(
'keydown'
,
'a.a11y-menu-item-link'
,
this
.
keyDownHandler
);
},
// Get previous element in array or cyles back to the last if it is the
// first.
previousMenuItemLink
:
function
(
links
,
index
)
{
return
index
<
1
?
links
.
last
()
:
links
.
eq
(
index
-
1
);
},
// Get next element in array or cyles back to the first if it is the last.
nextMenuItemLink
:
function
(
links
,
index
)
{
return
index
>=
links
.
length
-
1
?
links
.
first
()
:
links
.
eq
(
index
+
1
);
},
menuItemsLinksFocused
:
function
()
{
return
this
.
menuItemsLinks
.
is
(
':focus'
);
},
openMenu
:
function
(
withoutHandler
)
{
// When menu items have focus, the menu stays open on
// mouseleave. A closeMenuHandler is added to the window
// element to have clicks close the menu when they happen
// outside of it. We namespace the click event to easily remove it (and
// only it) in closeMenu.
this
.
el
.
addClass
(
'open'
);
this
.
button
.
text
(
'...'
);
if
(
!
withoutHandler
)
{
$
(
window
).
on
(
'click.currentMenu'
,
this
.
closeMenuHandler
);
}
// @TODO: onOpen callback
},
closeMenu
:
function
(
withoutHandler
)
{
// Remove the previously added clickHandler from window element.
var
msg
=
'.'
+
this
.
value
;
this
.
el
.
removeClass
(
'open'
);
this
.
button
.
text
(
gettext
(
msg
));
if
(
!
withoutHandler
)
{
$
(
window
).
off
(
'click.currentMenu'
);
}
// @TODO: onClose callback
this
.
el
=
this
.
container
.
find
(
'.list-download-transcripts'
);
this
.
el
.
on
(
'click'
,
'.btn-link'
,
this
.
clickHandler
);
},
openMenuHandler
:
function
()
{
this
.
openMenu
(
true
);
return
false
;
},
closeMenuHandler
:
function
(
event
)
{
// Only close the menu if no menu item link has focus or `click` event.
if
(
!
this
.
menuItemsLinksFocused
()
||
event
.
type
===
'click'
)
{
this
.
closeMenu
(
true
);
}
return
false
;
},
toggleMenuHandler
:
function
()
{
if
(
this
.
el
.
hasClass
(
'open'
))
{
this
.
closeMenu
(
true
);
}
else
{
this
.
openMenu
(
true
);
}
return
false
;
},
// Various event handlers. They all return false to stop propagation and
// prevent default behavior.
// Event handler. We delay link clicks until the file type is set
clickHandler
:
function
(
event
)
{
this
.
changeFileType
.
call
(
this
,
event
);
this
.
closeMenu
(
true
);
return
false
;
},
keyDownHandler
:
function
(
event
)
{
var
KEY
=
$
.
ui
.
keyCode
,
keyCode
=
event
.
keyCode
,
target
=
$
(
event
.
currentTarget
),
index
;
if
(
target
.
is
(
'a.a11y-menu-item-link'
))
{
index
=
target
.
parent
().
index
();
switch
(
keyCode
)
{
// Scroll up menu, wrapping at the top. Keep menu open.
case
KEY
.
UP
:
this
.
previousMenuItemLink
(
this
.
menuItemsLinks
,
index
).
focus
();
break
;
// Scroll down menu, wrapping at the bottom. Keep menu
// open.
case
KEY
.
DOWN
:
this
.
nextMenuItemLink
(
this
.
menuItemsLinks
,
index
).
focus
();
break
;
// Close menu.
case
KEY
.
TAB
:
this
.
closeMenu
();
// TODO
// What has to happen here? In speed menu, tabbing backward
// will give focus to Play/Pause button and tabbing
// forward to Volume button.
break
;
// Close menu, give focus to button and change
// file type.
case
KEY
.
ENTER
:
case
KEY
.
SPACE
:
this
.
button
.
focus
();
this
.
changeFileType
.
call
(
this
,
event
);
this
.
closeMenu
();
break
;
// Close menu and give focus to speed control.
case
KEY
.
ESCAPE
:
this
.
closeMenu
();
this
.
button
.
focus
();
break
;
}
return
false
;
}
else
{
switch
(
keyCode
)
{
// Open menu and focus on last element of list above it.
case
KEY
.
ENTER
:
case
KEY
.
SPACE
:
case
KEY
.
UP
:
this
.
openMenu
();
this
.
menuItemsLinks
.
last
().
focus
();
break
;
// Close menu.
case
KEY
.
ESCAPE
:
this
.
closeMenu
();
break
;
}
// We do not stop propagation and default behavior on a TAB
// keypress.
return
event
.
keyCode
===
KEY
.
TAB
;
}
},
setValue
:
function
(
value
)
{
this
.
value
=
value
;
this
.
menuItems
.
removeClass
(
'active'
)
.
find
(
"a[data-value='"
+
value
+
"']"
)
.
parent
()
.
addClass
(
'active'
);
},
var
that
=
this
,
fileType
,
data
,
downloadUrl
;
changeFileType
:
function
(
event
)
{
var
fileType
=
$
(
event
.
currentTarget
).
data
(
'value'
),
data
=
{
'transcript_download_format'
:
fileType
};
event
.
preventDefault
();
this
.
setValue
(
fileType
);
this
.
options
.
storage
.
setItem
(
'transcript_download_format'
,
fileType
);
fileType
=
$
(
event
.
target
).
data
(
'value'
);
data
=
{
transcript_download_format
:
fileType
};
downloadUrl
=
$
(
event
.
target
).
attr
(
'href'
);
$
.
ajax
({
url
:
this
.
options
.
saveStateUrl
,
type
:
'POST'
,
dataType
:
'json'
,
data
:
data
data
:
data
,
success
:
function
()
{
that
.
options
.
storage
.
setItem
(
'transcript_download_format'
,
fileType
);
},
complete
:
function
()
{
document
.
location
.
href
=
downloadUrl
;
}
});
}
};
return
Video
AccessibleMenu
;
return
Video
TranscriptDownloadHandler
;
});
}(
RequireJS
.
define
));
common/test/acceptance/pages/lms/video/video.py
View file @
0ebc9047
...
...
@@ -57,7 +57,10 @@ VIDEO_MENUS = {
'language'
:
'.lang .menu'
,
'speed'
:
'.speed .menu'
,
'download_transcript'
:
'.video-tracks .a11y-menu-list'
,
'transcript-format'
:
'.video-tracks .a11y-menu-button'
,
'transcript-format'
:
{
'srt'
:
'.wrapper-download-transcripts .list-download-transcripts .btn-link[data-value="srt"]'
,
'txt'
:
'.wrapper-download-transcripts .list-download-transcripts .btn-link[data-value="txt"]'
},
'transcript-skip'
:
'.sr-is-focusable.transcript-start'
,
}
...
...
@@ -584,7 +587,7 @@ class VideoPage(PageObject):
bool: Transcript download result.
"""
transcript_selector
=
self
.
get_element_selector
(
VIDEO_MENUS
[
'transcript-format'
])
transcript_selector
=
self
.
get_element_selector
(
VIDEO_MENUS
[
'transcript-format'
]
[
transcript_format
]
)
# check if we have a transcript with correct format
if
'.'
+
transcript_format
not
in
self
.
q
(
css
=
transcript_selector
)
.
text
[
0
]:
...
...
@@ -595,16 +598,15 @@ class VideoPage(PageObject):
'txt'
:
'text/plain'
,
}
transcript_url_selector
=
self
.
get_element_selector
(
VIDEO_BUTTONS
[
'download_transcript'
])
url
=
self
.
q
(
css
=
transcript_url_selector
)
.
attrs
(
'href'
)[
0
]
link
=
self
.
q
(
css
=
transcript_selector
)
url
=
link
.
attrs
(
'href'
)[
0
]
link
.
click
()
result
,
headers
,
content
=
self
.
_get_transcript
(
url
)
if
result
is
False
:
return
False
if
formats
[
transcript_format
]
not
in
headers
.
get
(
'content-type'
,
''
):
return
False
if
text_to_search
not
in
content
.
decode
(
'utf-8'
):
return
False
...
...
@@ -674,45 +676,6 @@ class VideoPage(PageObject):
selector
=
self
.
get_element_selector
(
VIDEO_MENUS
[
menu_name
])
return
self
.
q
(
css
=
selector
)
.
present
def
select_transcript_format
(
self
,
transcript_format
):
"""
Select transcript with format `transcript_format`.
Arguments:
transcript_format (st): Transcript file format `srt` or `txt`.
Returns:
bool: Selection Result.
"""
button_selector
=
self
.
get_element_selector
(
VIDEO_MENUS
[
'transcript-format'
])
button
=
self
.
q
(
css
=
button_selector
)
.
results
[
0
]
hover
=
ActionChains
(
self
.
browser
)
.
move_to_element
(
button
)
hover
.
perform
()
if
'...'
not
in
self
.
q
(
css
=
button_selector
)
.
text
[
0
]:
return
False
menu_selector
=
self
.
get_element_selector
(
VIDEO_MENUS
[
'download_transcript'
])
menu_items
=
self
.
q
(
css
=
menu_selector
+
' a'
)
.
results
for
item
in
menu_items
:
if
item
.
get_attribute
(
'data-value'
)
==
transcript_format
:
ActionChains
(
self
.
browser
)
.
move_to_element
(
item
)
.
click
()
.
perform
()
self
.
wait_for_ajax
()
break
self
.
browser
.
execute_script
(
"window.scrollTo(0, 0);"
)
if
self
.
q
(
css
=
menu_selector
+
' .active a'
)
.
attrs
(
'data-value'
)[
0
]
!=
transcript_format
:
return
False
if
'.'
+
transcript_format
not
in
self
.
q
(
css
=
button_selector
)
.
text
[
0
]:
return
False
return
True
@property
def
sources
(
self
):
"""
...
...
common/test/acceptance/pages/studio/video/video.py
View file @
0ebc9047
...
...
@@ -32,7 +32,7 @@ CLASS_SELECTORS = {
BUTTON_SELECTORS
=
{
'create_video'
:
'button[data-category="video"]'
,
'handout_download'
:
'.
video-handout.video-download-button a
'
,
'handout_download'
:
'.
wrapper-handouts .btn-link
'
,
'handout_download_editor'
:
'.wrapper-comp-setting.file-uploader .download-action'
,
'upload_asset'
:
'.upload-action'
,
'asset_submit'
:
'.action-upload'
,
...
...
common/test/acceptance/tests/video/test_video_module.py
View file @
0ebc9047
...
...
@@ -4,6 +4,7 @@
Acceptance tests for Video.
"""
import
os
from
ddt
import
ddt
,
unpack
,
data
from
mock
import
patch
from
nose.plugins.attrib
import
attr
...
...
@@ -199,6 +200,7 @@ class VideoBaseTest(UniqueCourseTest):
@attr
(
shard
=
4
)
@ddt
class
YouTubeVideoTest
(
VideoBaseTest
):
""" Test YouTube Video Player """
...
...
@@ -491,15 +493,16 @@ class YouTubeVideoTest(VideoBaseTest):
self
.
assertTrue
(
self
.
video
.
is_button_shown
(
'transcript_button'
))
self
.
_verify_caption_text
(
'Welcome to edX.'
)
def
test_download_transcript_button_works_correctly
(
self
):
@data
((
'srt'
,
'00:00:00,260'
),
(
'txt'
,
'Welcome to edX.'
))
@unpack
def
test_download_transcript_links_work_correctly
(
self
,
file_type
,
search_text
):
"""
Scenario: Download Transcript button works correctly
Scenario: Download 'srt' transcript link works correctly.
Download 'txt' transcript link works correctly.
Given the course has Video components A and B in "Youtube" mode
And Video component C in "HTML5" mode
And I have defined downloadable transcripts for the videos
Then I can download a transcript for Video A in "srt" format
And I can download a transcript for Video A in "txt" format
And I can download a transcript for Video B in "txt" format
And the Download Transcript menu does not exist for Video C
"""
...
...
@@ -524,19 +527,7 @@ class YouTubeVideoTest(VideoBaseTest):
self
.
navigate_to_video
()
# check if we can download transcript in "srt" format that has text "00:00:00,260"
self
.
assertTrue
(
self
.
video
.
downloaded_transcript_contains_text
(
'srt'
,
'00:00:00,260'
))
# select the transcript format "txt"
self
.
assertTrue
(
self
.
video
.
select_transcript_format
(
'txt'
))
# check if we can download transcript in "txt" format that has text "Welcome to edX."
self
.
assertTrue
(
self
.
video
.
downloaded_transcript_contains_text
(
'txt'
,
'Welcome to edX.'
))
# open vertical containing video "B"
self
.
course_nav
.
go_to_vertical
(
'Test Vertical-1'
)
# check if we can download transcript in "txt" format that has text "Equal transcripts"
self
.
assertTrue
(
self
.
video
.
downloaded_transcript_contains_text
(
'txt'
,
'Equal transcripts'
))
self
.
assertTrue
(
self
.
video
.
downloaded_transcript_contains_text
(
file_type
,
search_text
))
# open vertical containing video "C"
self
.
course_nav
.
go_to_vertical
(
'Test Vertical-2'
)
...
...
lms/templates/video.html
View file @
0ebc9047
...
...
@@ -39,52 +39,49 @@ from openedx.core.djangolib.js_utils import js_escaped_string
</div>
<div
class=
"focus_grabber last"
></div>
<ul
class=
"wrapper-downloads"
>
% if download_video_link or track or handout or branding_info:
<h3
class=
"hd hd-4 downloads-heading sr"
id=
"video-download-transcripts_${id}"
>
${_('Downloads and transcripts')}
</h3>
<div
class=
"wrapper-downloads"
role=
"region"
aria-labelledby=
"video-download-transcripts_${id}"
>
% if download_video_link:
<li
class=
"video-sources video-download-button"
>
<a
href=
"${download_video_link}"
>
${_('Download video')}
</a>
</li>
<div
class=
"wrapper-download-video"
>
<h4
class=
"hd hd-5"
>
${_('Video')}
</h4>
<a
class=
"btn-link video-sources video-download-button"
href=
"${download_video_link}"
>
${_('Download video file')}
</a>
</div>
% endif
% if track:
<li
class=
"video-tracks video-download-button"
>
<div
class=
"wrapper-download-transcripts"
>
<h4
class=
"hd hd-5"
>
${_('Transcripts')}
</h4>
% if transcript_download_format:
<a
href=
"${track}"
>
${_('Download transcript')}
</a>
<div
class=
"a11y-menu-container"
>
<a
class=
"a11y-menu-button"
href=
"#"
title=
"${'.' + transcript_download_format}"
role=
"button"
aria-disabled=
"false"
>
${'.' + transcript_download_format}
</a>
<ol
class=
"a11y-menu-list"
role=
"menu"
>
<ul
class=
"list-download-transcripts"
>
% for item in transcript_download_formats_list:
% if item['value'] == transcript_download_format:
<li
class=
"a11y-menu-item active"
>
% else:
<li
class=
"a11y-menu-item"
>
% endif
## This is necessary so we don't scrape 'display_name' as a string.
<
%
dname =
item['display_name']
%
>
<a
class=
"a11y-menu-item-link"
href=
"#${item['value']}"
title=
"${_(dname)}"
data-value=
"${item['value']}"
role=
"menuitem"
aria-disabled=
"false"
>
${_(dname)}
</a>
<li
class=
"transcript-option"
>
<
%
dname =
_("Download
{
file
}").
format
(
file=
item['display_name'])
%
>
<a
class=
"btn btn-link"
href=
"${track}"
data-value=
"${item['value']}"
>
${dname}
</a>
</li>
% endfor
</ol>
</div>
</ul>
% else:
<a
href=
"${track}"
class=
"external-track
"
>
${_('Download transcript')}
</a>
<a
class=
"btn-link external-track"
href=
"${track}
"
>
${_('Download transcript')}
</a>
% endif
</
li
>
</
div
>
% endif
% if handout:
<li
class=
"video-handout video-download-button"
>
<a
href=
"${handout}"
target=
"_blank"
>
${_('Download Handout')}
</a>
</li>
<div
class=
"wrapper-handouts"
>
<h4
class=
"hd hd-5"
>
${_('Handouts')}
</h4>
<a
class=
"btn-link"
href=
"${handout}"
>
${_('Download Handout')}
</a>
</div>
% endif
% if branding_info:
<li
id=
"branding"
class=
"branding"
>
<div
class=
"branding"
>
<span
class=
"host-tag"
>
${branding_info['logo_tag']}
</span>
<a
href=
"${branding_info['url']}"
target=
"_blank"
title=
"${branding_info['logo_tag']}"
><img
class=
"brand-logo"
src=
"${branding_info['logo_src']}"
alt=
"${branding_info['logo_tag']}"
/></a>
</li>
<a
href=
"${branding_info['url']}"
><img
class=
"brand-logo"
src=
"${branding_info['logo_src']}"
alt=
"${branding_info['logo_tag']}"
/></a>
</div>
% endif
</div>
% endif
</ul>
</div>
% if cdn_eval:
<script>
...
...
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