Add Timed Transcripts Editor.

This commit is contained in:
Anton Stupak
2013-08-13 18:10:44 +03:00
committed by polesye
parent 1f7bb112bf
commit aecc20af6b
75 changed files with 8762 additions and 279 deletions

View File

@@ -5,4 +5,5 @@
# Video are written in pure JavaScript.
!video/*.js
!video/*.js
!video/transcripts/*.js

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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):
"""