Add Timed Transcripts Editor.
This commit is contained in:
3
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
3
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
@@ -5,4 +5,5 @@
|
||||
|
||||
|
||||
# Video are written in pure JavaScript.
|
||||
!video/*.js
|
||||
!video/*.js
|
||||
!video/transcripts/*.js
|
||||
@@ -65,6 +65,15 @@ class @TabsEditingDescriptor
|
||||
current_tab = @$tabs.filter('.current').html()
|
||||
data: TabsEditingDescriptor.Model.getValue(@html_id, current_tab)
|
||||
|
||||
setMetadataEditor : (metadataEditor) ->
|
||||
TabsEditingDescriptor.setMetadataEditor.apply(TabsEditingDescriptor, arguments)
|
||||
|
||||
getStorage : () ->
|
||||
TabsEditingDescriptor.getStorage()
|
||||
|
||||
addToStorage : (id, data) ->
|
||||
TabsEditingDescriptor.addToStorage.apply(TabsEditingDescriptor, arguments)
|
||||
|
||||
@Model :
|
||||
addModelUpdate : (id, tabName, modelUpdateFunction) ->
|
||||
###
|
||||
@@ -115,6 +124,7 @@ class @TabsEditingDescriptor
|
||||
# html_id's of descriptors will be stored in modules variable as
|
||||
# containers for callbacks.
|
||||
modules: {}
|
||||
Storage: {}
|
||||
|
||||
initialize : (id) ->
|
||||
###
|
||||
@@ -123,3 +133,13 @@ class @TabsEditingDescriptor
|
||||
@modules[id] = @modules[id] or {}
|
||||
@modules[id].tabSwitch = @modules[id]['tabSwitch'] or {}
|
||||
@modules[id].modelUpdate = @modules[id]['modelUpdate'] or {}
|
||||
|
||||
@setMetadataEditor : (metadataEditor) ->
|
||||
TabsEditingDescriptor.Model.Storage['MetadataEditor'] = metadataEditor
|
||||
|
||||
@addToStorage : (id, data) ->
|
||||
TabsEditingDescriptor.Model.Storage[id] = data
|
||||
|
||||
@getStorage : () ->
|
||||
TabsEditingDescriptor.Model.Storage
|
||||
|
||||
|
||||
@@ -141,9 +141,13 @@ class VideoDescriptorTest(unittest.TestCase):
|
||||
""""test get_context"""
|
||||
correct_tabs = [
|
||||
{
|
||||
'name': "Settings",
|
||||
'template': "tabs/metadata-edit-tab.html",
|
||||
'name': "Basic",
|
||||
'template': "video/transcripts.html",
|
||||
'current': True
|
||||
},
|
||||
{
|
||||
'name': 'Advanced',
|
||||
'template': 'tabs/metadata-edit-tab.html'
|
||||
}
|
||||
]
|
||||
rendered_context = self.descriptor.get_context()
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import urlparse
|
||||
import mock
|
||||
import threading
|
||||
import json
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
import time
|
||||
|
||||
class MockYoutubeRequestHandler(BaseHTTPRequestHandler):
|
||||
'''
|
||||
A handler for Youtube GET requests.
|
||||
'''
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def do_HEAD(self):
|
||||
code = 200
|
||||
if 'test_transcripts_youtube' in self.path:
|
||||
if not 'trans_exist' in self.path:
|
||||
code = 404
|
||||
self._send_head(code)
|
||||
|
||||
def do_GET(self):
|
||||
'''
|
||||
Handle a GET request from the client and sends response back.
|
||||
'''
|
||||
logger.debug("Youtube provider received GET request to path {}".format(
|
||||
self.path)
|
||||
) # Log the request
|
||||
|
||||
if 'test_transcripts_youtube' in self.path:
|
||||
if 't__eq_exist' in self.path:
|
||||
status_message = """<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.0" dur="1.0">Equal transcripts</text></transcript>"""
|
||||
self._send_head()
|
||||
self._send_transcripts_response(status_message)
|
||||
elif 't_neq_exist' in self.path:
|
||||
status_message = """<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.1" dur="5.5">Transcripts sample, different that on server</text></transcript>"""
|
||||
self._send_head()
|
||||
self._send_transcripts_response(status_message)
|
||||
else:
|
||||
self._send_head(404)
|
||||
elif 'test_youtube' in self.path:
|
||||
self._send_head()
|
||||
#testing videoplayers
|
||||
status_message = "I'm youtube."
|
||||
response_timeout = float(self.server.time_to_response)
|
||||
|
||||
# threading timer produces TypeError: 'NoneType' object is not callable here
|
||||
# so we use time.sleep, as we already in separate thread.
|
||||
time.sleep(response_timeout)
|
||||
self._send_video_response(status_message)
|
||||
else:
|
||||
# unused url
|
||||
self._send_head()
|
||||
self._send_transcripts_response('Unused url')
|
||||
logger.debug("Request to unused url.")
|
||||
|
||||
def _send_head(self, code=200):
|
||||
'''
|
||||
Send the response code and MIME headers
|
||||
'''
|
||||
|
||||
self.send_response(code)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
|
||||
def _send_transcripts_response(self, message):
|
||||
'''
|
||||
Send message back to the client for transcripts ajax requests.
|
||||
'''
|
||||
response = message
|
||||
# Log the response
|
||||
logger.debug("Youtube: sent response {}".format(message))
|
||||
|
||||
self.wfile.write(response)
|
||||
|
||||
def _send_video_response(self, message):
|
||||
'''
|
||||
Send message back to the client for video player requests.
|
||||
Requires sending back callback id.
|
||||
'''
|
||||
callback = urlparse.parse_qs(self.path)['callback'][0]
|
||||
response = callback + '({})'.format(json.dumps({'message': message}))
|
||||
# Log the response
|
||||
logger.debug("Youtube: sent response {}".format(message))
|
||||
|
||||
self.wfile.write(response)
|
||||
|
||||
|
||||
class MockYoutubeServer(HTTPServer):
|
||||
'''
|
||||
A mock Youtube provider server that responds
|
||||
to GET requests to localhost.
|
||||
'''
|
||||
|
||||
def __init__(self, address):
|
||||
'''
|
||||
Initialize the mock XQueue server instance.
|
||||
|
||||
*address* is the (host, host's port to listen to) tuple.
|
||||
'''
|
||||
handler = MockYoutubeRequestHandler
|
||||
HTTPServer.__init__(self, address, handler)
|
||||
|
||||
def shutdown(self):
|
||||
'''
|
||||
Stop the server and free up the port
|
||||
'''
|
||||
# First call superclass shutdown()
|
||||
HTTPServer.shutdown(self)
|
||||
# We also need to manually close the socket
|
||||
self.socket.close()
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Test for Mock_Youtube_Server
|
||||
"""
|
||||
import unittest
|
||||
import threading
|
||||
import requests
|
||||
from mock_youtube_server import MockYoutubeServer
|
||||
|
||||
|
||||
class MockYoutubeServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the YouTube provider server that listens on a local
|
||||
port and responds with jsonp.
|
||||
|
||||
Used for lettuce BDD tests in lms/courseware/features/video.feature
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Create the server
|
||||
server_port = 8034
|
||||
server_host = '127.0.0.1'
|
||||
address = (server_host, server_port)
|
||||
self.server = MockYoutubeServer(address, )
|
||||
self.server.time_to_response = 0.5
|
||||
# Start the server in a separate daemon thread
|
||||
server_thread = threading.Thread(target=self.server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
# Stop the server, freeing up the port
|
||||
self.server.shutdown()
|
||||
|
||||
def test_request(self):
|
||||
"""
|
||||
Tests that Youtube server processes request with right program
|
||||
path, and responses with incorrect signature.
|
||||
"""
|
||||
# GET request
|
||||
|
||||
# unused url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/some url',
|
||||
)
|
||||
self.assertEqual("Unused url", response.content)
|
||||
|
||||
# video player test url, callback shoud be presented in url params
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func',
|
||||
)
|
||||
self.assertEqual("""callback_func({"message": "I\'m youtube."})""", response.content)
|
||||
|
||||
# transcripts test url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/t__eq_exist',
|
||||
)
|
||||
self.assertEqual(
|
||||
'<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.0" dur="1.0">Equal transcripts</text></transcript>',
|
||||
response.content
|
||||
)
|
||||
|
||||
# transcripts test url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/t_neq_exist',
|
||||
)
|
||||
self.assertEqual(
|
||||
'<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.1" dur="5.5">Transcripts sample, different that on server</text></transcript>',
|
||||
response.content
|
||||
)
|
||||
|
||||
# transcripts test url, not trans_exist youtube_id, so 404 should be returned
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/some_id',
|
||||
)
|
||||
self.assertEqual(404, response.status_code)
|
||||
@@ -17,9 +17,11 @@ from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
import datetime
|
||||
import time
|
||||
import copy
|
||||
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.editing_module import TabsEditingDescriptor
|
||||
@@ -30,7 +32,6 @@ from xblock.fields import Scope, String, Boolean, Float, List, Integer, ScopeIds
|
||||
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
|
||||
from xblock.runtime import DbModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -48,7 +49,7 @@ class VideoFields(object):
|
||||
)
|
||||
show_captions = Boolean(
|
||||
help="This controls whether or not captions are shown by default.",
|
||||
display_name="Show Captions",
|
||||
display_name="Show Transcript",
|
||||
scope=Scope.settings,
|
||||
default=True
|
||||
)
|
||||
@@ -103,13 +104,13 @@ class VideoFields(object):
|
||||
)
|
||||
track = String(
|
||||
help="The external URL to download the timed transcript track. This appears as a link beneath the video.",
|
||||
display_name="Download Track",
|
||||
display_name="Download Transcript",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
sub = String(
|
||||
help="The name of the timed transcript track (for non-Youtube videos).",
|
||||
display_name="HTML5 Timed Transcript",
|
||||
display_name="HTML5 Transcript",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
@@ -196,14 +197,14 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
module_class = VideoModule
|
||||
|
||||
tabs = [
|
||||
# {
|
||||
# 'name': "Subtitles",
|
||||
# 'template': "video/subtitles.html",
|
||||
# },
|
||||
{
|
||||
'name': "Settings",
|
||||
'template': "tabs/metadata-edit-tab.html",
|
||||
'name': "Basic",
|
||||
'template': "video/transcripts.html",
|
||||
'current': True
|
||||
},
|
||||
{
|
||||
'name': "Advanced",
|
||||
'template': "tabs/metadata-edit-tab.html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -286,6 +287,45 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
xml.append(ele)
|
||||
return xml
|
||||
|
||||
def get_context(self):
|
||||
"""
|
||||
Extend context by data for transcripts basic tab.
|
||||
"""
|
||||
_context = super(VideoDescriptor, self).get_context()
|
||||
|
||||
metadata_fields = copy.deepcopy(self.editable_metadata_fields)
|
||||
|
||||
display_name = metadata_fields['display_name']
|
||||
video_url = metadata_fields['html5_sources']
|
||||
youtube_id_1_0 = metadata_fields['youtube_id_1_0']
|
||||
|
||||
def get_youtube_link(video_id):
|
||||
if video_id:
|
||||
return 'http://youtu.be/{0}'.format(video_id)
|
||||
else:
|
||||
return ''
|
||||
|
||||
video_url.update({
|
||||
'help': _('A YouTube URL or a link to a file hosted anywhere on the web.'),
|
||||
'display_name': 'Video URL',
|
||||
'field_name': 'video_url',
|
||||
'type': 'VideoList',
|
||||
'default_value': [get_youtube_link(youtube_id_1_0['default_value'])]
|
||||
})
|
||||
|
||||
youtube_id_1_0_value = get_youtube_link(youtube_id_1_0['value'])
|
||||
|
||||
if youtube_id_1_0_value:
|
||||
video_url['value'].insert(0, youtube_id_1_0_value)
|
||||
|
||||
metadata = {
|
||||
'display_name': display_name,
|
||||
'video_url': video_url
|
||||
}
|
||||
|
||||
_context.update({'transcripts_basic_tab_metadata': metadata})
|
||||
return _context
|
||||
|
||||
@classmethod
|
||||
def _parse_youtube(cls, data):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user