", {id: "metadata-videolist-entry"})
+ .html(videoListEntryTemplate)
+ );
+ appendSetFixtures(
+ $("
+
% if context_course:
+% endfor
+
+% for template_name in ["transcripts-found", "transcripts-uploaded", "transcripts-use-existing", "transcripts-not-found", "transcripts-replace", "transcripts-import", "transcripts-choose"]:
+
+% endfor
+
+
+
+
diff --git a/cms/urls.py b/cms/urls.py
index 1cf92b8d81a0..e13a87602fd4 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -21,6 +21,15 @@
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_item'),
url(r'^create_item$', 'contentstore.views.create_item', name='create_item'),
+
+ url(r'^transcripts/upload$', 'contentstore.views.upload_transcripts', name='upload_transcripts'),
+ url(r'^transcripts/download$', 'contentstore.views.download_transcripts', name='download_transcripts'),
+ url(r'^transcripts/check$', 'contentstore.views.check_transcripts', name='check_transcripts'),
+ url(r'^transcripts/choose$', 'contentstore.views.choose_transcripts', name='choose_transcripts'),
+ url(r'^transcripts/replace$', 'contentstore.views.replace_transcripts', name='replace_transcripts'),
+ url(r'^transcripts/rename$', 'contentstore.views.rename_transcripts', name='rename_transcripts'),
+ url(r'^transcripts/save$', 'contentstore.views.save_transcripts', name='save_transcripts'),
+
url(r'^create_draft$', 'contentstore.views.create_draft', name='create_draft'),
url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'),
url(r'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'),
diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py
index 86f04198492e..d5abb6260cd9 100644
--- a/common/djangoapps/terrain/steps.py
+++ b/common/djangoapps/terrain/steps.py
@@ -212,3 +212,11 @@ def i_answer_prompts_with(step, prompt):
In addition, this method changes the functionality of ONLY future alerts
"""
world.browser.execute_script('window.prompt = function(){return %s;}') % prompt
+
+
+@step('I run ipdb')
+def run_ipdb(_step):
+ """Run ipdb as step for easy debugging"""
+ import ipdb
+ ipdb.set_trace()
+ assert True
diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py
index 92e7a0425b2a..14ce2d7a5846 100644
--- a/common/djangoapps/terrain/ui_helpers.py
+++ b/common/djangoapps/terrain/ui_helpers.py
@@ -467,6 +467,11 @@ def click_link(partial_text, index=0):
wait_for_js_to_load()
+@world.absorb
+def click_link_by_text(text, index=0):
+ retry_on_exception(lambda: world.browser.find_link_by_text(text)[index].click())
+
+
@world.absorb
def css_text(css_selector, index=0, timeout=30):
# Wait for the css selector to appear
diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore
index 8978489f6f38..54af6653e900 100644
--- a/common/lib/xmodule/xmodule/js/src/.gitignore
+++ b/common/lib/xmodule/xmodule/js/src/.gitignore
@@ -5,4 +5,5 @@
# Video are written in pure JavaScript.
-!video/*.js
\ No newline at end of file
+!video/*.js
+!video/transcripts/*.js
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee b/common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee
index 521b19dfbc33..696764a58bcb 100644
--- a/common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee
+++ b/common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee
@@ -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
+
diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py
index 4c9972ea3ad2..b6475c22e194 100644
--- a/common/lib/xmodule/xmodule/tests/test_video.py
+++ b/common/lib/xmodule/xmodule/tests/test_video.py
@@ -141,9 +141,13 @@ def test_get_context(self):
""""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()
diff --git a/lms/djangoapps/courseware/mock_youtube_server/__init__.py b/common/lib/xmodule/xmodule/util/mock_youtube_server/__init__.py
similarity index 100%
rename from lms/djangoapps/courseware/mock_youtube_server/__init__.py
rename to common/lib/xmodule/xmodule/util/mock_youtube_server/__init__.py
diff --git a/common/lib/xmodule/xmodule/util/mock_youtube_server/mock_youtube_server.py b/common/lib/xmodule/xmodule/util/mock_youtube_server/mock_youtube_server.py
new file mode 100644
index 000000000000..1a4c08cd42d3
--- /dev/null
+++ b/common/lib/xmodule/xmodule/util/mock_youtube_server/mock_youtube_server.py
@@ -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 = """
Equal transcripts"""
+ self._send_head()
+ self._send_transcripts_response(status_message)
+ elif 't_neq_exist' in self.path:
+ status_message = """
Transcripts sample, different that on server"""
+ 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()
diff --git a/common/lib/xmodule/xmodule/util/mock_youtube_server/test_mock_youtube_server.py b/common/lib/xmodule/xmodule/util/mock_youtube_server/test_mock_youtube_server.py
new file mode 100644
index 000000000000..bb657609053c
--- /dev/null
+++ b/common/lib/xmodule/xmodule/util/mock_youtube_server/test_mock_youtube_server.py
@@ -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(
+ '
Equal transcripts',
+ response.content
+ )
+
+ # transcripts test url
+ response = requests.get(
+ 'http://127.0.0.1:8034/test_transcripts_youtube/t_neq_exist',
+ )
+ self.assertEqual(
+ '
Transcripts sample, different that on server',
+ 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)
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index b0d0dde4703d..eccad071e9c6 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -17,9 +17,11 @@
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 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 @@ def definition_to_xml(self, resource_fs):
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):
"""
diff --git a/common/static/js/vendor/jquery.ajaxQueue.js b/common/static/js/vendor/jquery.ajaxQueue.js
new file mode 100644
index 000000000000..13c3aa4f56be
--- /dev/null
+++ b/common/static/js/vendor/jquery.ajaxQueue.js
@@ -0,0 +1,53 @@
+/*
+* jQuery.ajaxQueue - A queue for ajax requests
+*
+* @copyright: Copyright (c) 2013 Corey Frang
+* @license: Licensed under the MIT license. See https://github.com/gnarf/jquery-ajaxQueue/blob/master/LICENSE-MIT.
+* @website: https://github.com/gnarf/jquery-ajaxQueue
+*/
+(function($) {
+
+// jQuery on an empty object, we are going to use this as our Queue
+var ajaxQueue = $({});
+
+$.ajaxQueue = function( ajaxOpts ) {
+ var jqXHR,
+ dfd = $.Deferred(),
+ promise = dfd.promise();
+
+ // run the actual query
+ function doRequest( next ) {
+ jqXHR = $.ajax( ajaxOpts );
+ jqXHR.done( dfd.resolve )
+ .fail( dfd.reject )
+ .then( next, next );
+ }
+
+ // queue our ajax request
+ ajaxQueue.queue( doRequest );
+
+ // add the abort method
+ promise.abort = function( statusText ) {
+
+ // proxy abort to the jqXHR if it is active
+ if ( jqXHR ) {
+ return jqXHR.abort( statusText );
+ }
+
+ // if there wasn't already a jqXHR we need to remove from queue
+ var queue = ajaxQueue.queue(),
+ index = $.inArray( doRequest, queue );
+
+ if ( index > -1 ) {
+ queue.splice( index, 1 );
+ }
+
+ // and then reject the deferred
+ dfd.rejectWith( ajaxOpts.context || ajaxOpts, [ promise, statusText, "" ] );
+ return promise;
+ };
+
+ return promise;
+};
+
+})(jQuery);
diff --git a/common/test/data/uploads/chinese_transcripts.srt b/common/test/data/uploads/chinese_transcripts.srt
new file mode 100644
index 000000000000..4bbbea38fa7a
--- /dev/null
+++ b/common/test/data/uploads/chinese_transcripts.srt
@@ -0,0 +1,1092 @@
+1
+00:00:00,293 --> 00:00:01,245
+好 各位同学
+
+2
+00:00:01,493 --> 00:00:03,821
+我们今天要讲的题目是
+
+3
+00:00:04,037 --> 00:00:05,813
+从算筹到ENIAC
+
+4
+00:00:06,181 --> 00:00:07,347
+那么今天的主要内容
+
+5
+00:00:07,733 --> 00:00:10,904
+我会从远古的手动的一些计算工具
+
+6
+00:00:11,197 --> 00:00:14,445
+一直讲到我们现代的电子计算机ENIAC
+
+7
+00:00:15,165 --> 00:00:18,925
+首先我们来介绍一下远古的计算工具
+
+8
+00:00:19,221 --> 00:00:21,125
+我们看
+
+9
+00:00:21,491 --> 00:00:24,829
+我们人类社会最早的计算工具是什么呢
+
+10
+00:00:25,126 --> 00:00:27,365
+其实早先可以有结绳记事
+
+11
+00:00:27,749 --> 00:00:28,789
+但是那个呢谈不上计算
+
+12
+00:00:29,221 --> 00:00:30,989
+只能称得上是存储
+
+13
+00:00:31,405 --> 00:00:32,653
+那么最早的计算工具
+
+14
+00:00:32,877 --> 00:00:36,149
+是出现在中国的商周时期
+
+15
+00:00:36,357 --> 00:00:37,063
+是什么呢
+
+16
+00:00:37,357 --> 00:00:39,805
+就是这样的一些东西
+
+17
+00:00:40,117 --> 00:00:40,965
+叫算筹
+
+18
+00:00:41,320 --> 00:00:42,557
+那么我们古代成语
+
+19
+00:00:43,021 --> 00:00:44,445
+“运筹帷幄”之中的“筹”
+
+20
+00:00:44,741 --> 00:00:46,141
+就是这个算筹
+
+21
+00:00:47,729 --> 00:00:49,109
+这是普通的算筹
+
+22
+00:00:49,373 --> 00:00:50,109
+就是一些小木棍
+
+23
+00:00:50,557 --> 00:00:52,173
+它的高端产品是什么呢
+
+24
+00:00:52,509 --> 00:00:53,869
+就是一些小骨头棍
+
+25
+00:00:54,205 --> 00:00:57,237
+这个算筹用来怎么计数呢
+
+26
+00:00:57,526 --> 00:01:00,741
+我们古代有“横式”“纵式”两种计数方式
+
+27
+00:01:01,077 --> 00:01:04,477
+其实就是不同的排放组合来代表不同的数字
+
+28
+00:01:05,109 --> 00:01:06,125
+那么大家想
+
+29
+00:01:06,405 --> 00:01:09,509
+这些小木棍用来作为计算工具
+
+30
+00:01:09,821 --> 00:01:13,253
+能够完成什么样的工作呢
+
+31
+00:01:13,565 --> 00:01:18,013
+事实上我们中国古代有一位老大爷
+
+32
+00:01:18,261 --> 00:01:21,389
+他用算筹还是做了一些工作
+
+33
+00:01:21,702 --> 00:01:23,173
+我们来认识一下这位老大爷
+
+34
+00:01:25,422 --> 00:01:26,149
+他是谁呢
+
+35
+00:01:26,613 --> 00:01:27,691
+他叫祖冲之
+
+36
+00:01:28,582 --> 00:01:30,501
+他用算筹做出了什么呢
+
+37
+00:01:30,820 --> 00:01:36,453
+他把圆周率从3.1415926到3.1415927之间估算出来
+
+38
+00:01:36,845 --> 00:01:41,333
+所以算筹还是可以做一些很好的工作的
+
+39
+00:01:42,181 --> 00:01:44,533
+同样的这个年代
+
+40
+00:01:44,845 --> 00:01:47,807
+在欧洲也有类似的一些工具
+
+41
+00:01:48,253 --> 00:01:50,493
+那就是欧洲的Napier算筹
+
+42
+00:01:51,157 --> 00:01:52,413
+欧洲的这个Napier算筹
+
+43
+00:01:52,661 --> 00:01:54,989
+它是依据一定的计算原理来做的
+
+44
+00:01:55,317 --> 00:01:56,030
+什么原理呢
+
+45
+00:01:56,373 --> 00:01:57,101
+我说是“格子原理”
+
+46
+00:01:57,501 --> 00:01:58,693
+那什么是“格子原理”呢
+
+47
+00:01:58,965 --> 00:02:00,091
+我们来看一下
+
+48
+00:02:00,462 --> 00:02:01,157
+举个简单例子
+
+49
+00:02:01,541 --> 00:02:04,205
+比如说我们要计算24乘以36
+
+50
+00:02:05,585 --> 00:02:07,765
+这个时候我们用一个格子把它画出来
+
+51
+00:02:08,277 --> 00:02:10,933
+把2、4、3、6分别对应横向的格和纵向的格
+
+52
+00:02:11,597 --> 00:02:14,837
+这时候我们把每一个格子分成两个部分
+
+53
+00:02:15,197 --> 00:02:16,917
+其中要填上数字
+
+54
+00:02:17,237 --> 00:02:17,775
+填什么数字呢
+
+55
+00:02:18,037 --> 00:02:19,357
+比如说2乘以6
+
+56
+00:02:21,234 --> 00:02:22,461
+它的这个相应的位置
+
+57
+00:02:22,901 --> 00:02:23,862
+2乘以6等于12
+
+58
+00:02:24,165 --> 00:02:26,021
+相应的位置就填上1和2
+
+59
+00:02:27,117 --> 00:02:28,117
+类似的方法
+
+60
+00:02:28,461 --> 00:02:30,013
+我们把所有的格子的数字都填满
+
+61
+00:02:30,525 --> 00:02:32,949
+那么好 24乘36到底等于多少
+
+62
+00:02:33,269 --> 00:02:36,533
+我们看 个位数就是4
+
+63
+00:02:36,776 --> 00:02:39,741
+十位数呢就是这样相加 就是6
+
+64
+00:02:40,037 --> 00:02:42,517
+百位数就是这样相加 就是8
+
+65
+00:02:42,864 --> 00:02:45,981
+864 大家去验算一下是不是对的
+
+66
+00:02:46,525 --> 00:02:47,638
+是对的
+
+67
+00:02:48,581 --> 00:02:50,181
+利用这样的一个“格子原理”
+
+68
+00:02:50,525 --> 00:02:53,829
+欧洲的Napier发明了这种算筹
+
+69
+00:02:54,173 --> 00:02:55,277
+就是这个样子的
+
+70
+00:02:55,603 --> 00:02:57,837
+大家看 就是这样
+
+71
+00:03:00,402 --> 00:03:03,605
+后来中国又出现了一种
+
+72
+00:03:03,893 --> 00:03:06,644
+低碳 环保 便携
+
+73
+00:03:07,229 --> 00:03:09,540
+同时解决问题又非常利索
+
+74
+00:03:09,797 --> 00:03:11,965
+三下五除二就可以搞定的计算工具
+
+75
+00:03:12,557 --> 00:03:13,277
+是什么呢
+
+76
+00:03:13,904 --> 00:03:15,079
+就是算盘
+
+77
+00:03:16,205 --> 00:03:18,709
+这个算盘应该说在中国古代
+
+78
+00:03:19,021 --> 00:03:21,709
+社会发展中起到了重要的作用
+
+79
+00:03:22,060 --> 00:03:22,789
+就不用多说了
+
+80
+00:03:25,157 --> 00:03:27,445
+它出现在宋 元 大概这个年代
+
+81
+00:03:29,189 --> 00:03:31,900
+还是大概同一时代
+
+82
+00:03:32,597 --> 00:03:34,061
+在欧洲有一个数学家
+
+83
+00:03:34,853 --> 00:03:36,420
+英国的数学家奥特雷德
+
+84
+00:03:36,842 --> 00:03:40,629
+他发明了一种计算工具叫“计算尺”
+
+85
+00:03:41,701 --> 00:03:43,324
+这个计算尺实际上
+
+86
+00:03:43,660 --> 00:03:45,772
+尽管是在十五世纪发明的
+
+87
+00:03:46,029 --> 00:03:48,277
+但是真正推广应用的
+
+88
+00:03:48,956 --> 00:03:49,941
+让世人所知的
+
+89
+00:03:50,221 --> 00:03:51,060
+是谁呢
+
+90
+00:03:51,364 --> 00:03:53,381
+是到了十八世纪的时候
+
+91
+00:03:53,820 --> 00:03:54,412
+一位人物
+
+92
+00:03:54,717 --> 00:03:55,495
+叫做瓦特
+
+93
+00:03:56,509 --> 00:03:58,717
+就是发明蒸汽机的瓦特
+
+94
+00:03:59,564 --> 00:04:01,565
+他把计算尺做了一点点改进
+
+95
+00:04:01,893 --> 00:04:02,317
+怎么改进呢
+
+96
+00:04:02,821 --> 00:04:03,573
+大家看
+
+97
+00:04:03,941 --> 00:04:09,076
+计算尺上增加了一个滑动的标
+
+98
+00:04:09,405 --> 00:04:11,645
+这个标是用来作为
+
+99
+00:04:12,052 --> 00:04:14,901
+记录中间的计算结果用的
+
+100
+00:04:15,652 --> 00:04:17,596
+所以瓦特用它做了大量的计算
+
+101
+00:04:17,949 --> 00:04:20,789
+也为后来的工业发展起到了重要作用
+
+102
+00:04:21,348 --> 00:04:22,740
+这是计算尺
+
+103
+00:04:24,445 --> 00:04:25,973
+这是远古的计算工具
+
+104
+00:04:26,261 --> 00:04:27,348
+我们说远古的计算工具
+
+105
+00:04:27,724 --> 00:04:30,116
+大体可以称为是手动计算工具
+
+106
+00:04:30,869 --> 00:04:32,772
+后来又有一些先辈
+
+107
+00:04:33,196 --> 00:04:34,365
+他们利用他们的智慧
+
+108
+00:04:34,748 --> 00:04:38,386
+为我们发明了机械式的计算工具
+
+109
+00:04:38,893 --> 00:04:40,469
+下面呢我们来看一下
+
+110
+00:04:40,837 --> 00:04:41,740
+机械式计算工具
+
+111
+00:04:44,260 --> 00:04:47,052
+在这一部分我要讲四个案例
+
+112
+00:04:47,716 --> 00:04:50,224
+按照历史的顺序我要将四个案例
+
+113
+00:04:50,772 --> 00:04:52,564
+其实我们还有一些别的计算工具
+
+114
+00:04:53,244 --> 00:04:54,724
+我们首先来看
+
+115
+00:04:55,988 --> 00:04:59,332
+在欧洲文艺复兴的期间
+
+116
+00:05:00,294 --> 00:05:05,103
+有一位老人家设计了这样的一个装置
+
+117
+00:05:05,670 --> 00:05:09,102
+由十三个齿轮来做加法
+
+118
+00:05:10,078 --> 00:05:10,998
+这样一个装置
+
+119
+00:05:11,518 --> 00:05:13,238
+那么这个装置实际的形状
+
+120
+00:05:13,886 --> 00:05:14,622
+就是这样的形状
+
+121
+00:05:14,750 --> 00:05:17,084
+那么是哪一位老人家设计的呢
+
+122
+00:05:17,924 --> 00:05:18,804
+我们来认识一下
+
+123
+00:05:21,364 --> 00:05:22,356
+哇 蒙娜丽莎
+
+124
+00:05:23,084 --> 00:05:24,012
+的作者达芬奇
+
+125
+00:05:25,716 --> 00:05:27,524
+他设计的这个装置
+
+126
+00:05:29,756 --> 00:05:32,908
+一直是停留在手稿状态
+
+127
+00:05:33,692 --> 00:05:35,268
+后来无意当中被人发现
+
+128
+00:05:35,837 --> 00:05:37,268
+说达芬奇怎么还有这么一个发明
+
+129
+00:05:38,581 --> 00:05:39,852
+但是一直到1968年
+
+130
+00:05:40,284 --> 00:05:41,748
+才有人真正把他这个装置
+
+131
+00:05:42,148 --> 00:05:43,884
+按照他的这个说明恢复了出来
+
+132
+00:05:44,348 --> 00:05:45,332
+就是下面这个图
+
+133
+00:05:45,996 --> 00:05:47,516
+这个图大家可以看到
+
+134
+00:05:48,004 --> 00:05:48,412
+就是一个真实的装置
+
+135
+00:05:48,935 --> 00:05:51,380
+你们在某些博物馆也许会看到这个装置
+
+136
+00:05:51,828 --> 00:05:56,204
+这是最早达芬奇做了一个加法器的设计
+
+137
+00:05:56,852 --> 00:05:57,316
+是一个机械式的
+
+138
+00:05:58,291 --> 00:06:00,772
+这个呢大约是在十四世纪的时候
+
+139
+00:06:01,116 --> 00:06:04,180
+又过了大约两百年
+
+140
+00:06:05,700 --> 00:06:07,933
+一位德国的科学家叫契克卡德
+
+141
+00:06:08,836 --> 00:06:12,620
+他又设计了一个机械式的计算装置
+
+142
+00:06:13,636 --> 00:06:14,616
+这是契克卡德
+
+143
+00:06:15,021 --> 00:06:18,428
+他发明的这个装置是这个样子的
+
+144
+00:06:19,268 --> 00:06:19,957
+为什么这样呢?
+
+145
+00:06:20,164 --> 00:06:22,476
+因为他最早发明这个装置是木头的
+
+146
+00:06:23,108 --> 00:06:24,005
+放到他的家乡
+
+147
+00:06:25,445 --> 00:06:26,604
+结果就因为是木头的
+
+148
+00:06:27,015 --> 00:06:28,452
+有一次家乡不小心失火了
+
+149
+00:06:29,748 --> 00:06:30,452
+一把火烧掉了
+
+150
+00:06:31,356 --> 00:06:32,284
+所以留下的只有图纸
+
+151
+00:06:33,212 --> 00:06:35,228
+后人又根据他的图纸
+
+152
+00:06:35,916 --> 00:06:37,277
+真实地再现了
+
+153
+00:06:37,917 --> 00:06:39,278
+他所设计的这个计算装置
+
+154
+00:06:40,284 --> 00:06:40,989
+也还是木头的
+
+155
+00:06:41,572 --> 00:06:43,796
+发现这个装置运行非常好
+
+156
+00:06:45,220 --> 00:06:46,692
+这是德国的科学家当时
+
+157
+00:06:47,677 --> 00:06:50,004
+用木头做的一个计算装置
+
+158
+00:06:51,101 --> 00:06:54,789
+大体上和契克卡德在同一年代
+
+159
+00:06:55,573 --> 00:06:57,149
+又有一位年轻人
+
+160
+00:06:57,565 --> 00:07:00,709
+他在19岁的时候设计了一个
+
+161
+00:07:01,085 --> 00:07:03,381
+也是机械式计算装置
+
+162
+00:07:03,925 --> 00:07:05,013
+叫齿轮计算器
+
+163
+00:07:05,669 --> 00:07:06,317
+就是这样的
+
+164
+00:07:06,661 --> 00:07:07,765
+这个计算器的特点是什么
+
+165
+00:07:08,173 --> 00:07:10,325
+十进制 带进位
+
+166
+00:07:10,989 --> 00:07:11,669
+这样的一个特点
+
+167
+00:07:12,029 --> 00:07:14,189
+这个年轻人为什么要设计这样一个装置呢
+
+168
+00:07:14,989 --> 00:07:18,365
+是因为他的父亲是政府官员
+
+169
+00:07:18,901 --> 00:07:22,725
+负责的工作是每天都要计算复杂的税率
+
+170
+00:07:23,293 --> 00:07:24,805
+计算任务非常重
+
+171
+00:07:25,490 --> 00:07:28,309
+他年纪轻轻的时候就觉得
+
+172
+00:07:28,685 --> 00:07:29,485
+父亲很辛苦
+
+173
+00:07:29,869 --> 00:07:32,277
+我希望能够给父亲做一点事情
+
+174
+00:07:32,613 --> 00:07:33,221
+所以他就想
+
+175
+00:07:33,509 --> 00:07:34,661
+我可不可以用一个机械的装置
+
+176
+00:07:35,013 --> 00:07:36,741
+来代替父亲的这种繁琐的工作
+
+177
+00:07:37,069 --> 00:07:40,789
+所以他就设计了这么一个装置
+
+178
+00:07:41,117 --> 00:07:42,629
+叫齿轮计算器
+
+179
+00:07:44,803 --> 00:07:45,547
+谁设计的呢
+
+180
+00:07:45,771 --> 00:07:46,569
+就是这位年轻人
+
+181
+00:07:46,875 --> 00:07:47,880
+大家可能就不太认识
+
+182
+00:07:48,253 --> 00:07:50,701
+这位年轻人的名字叫做帕斯卡
+
+183
+00:07:51,952 --> 00:07:53,488
+大家觉得耳熟啊
+
+184
+00:07:53,957 --> 00:07:55,149
+帕斯卡是谁呢
+
+185
+00:07:55,525 --> 00:07:57,437
+没错 就是你想的那个压强的单位
+
+186
+00:07:57,789 --> 00:07:58,269
+帕斯卡
+
+187
+00:07:58,989 --> 00:08:02,229
+他最早设计了这个齿轮式的计算器
+
+188
+00:08:03,725 --> 00:08:05,837
+这个计算器的实物是这样的
+
+189
+00:08:06,637 --> 00:08:07,405
+它最后生产没有呢
+
+190
+00:08:07,797 --> 00:08:08,677
+生产了
+
+191
+00:08:08,965 --> 00:08:09,693
+而且生产了很多
+
+192
+00:08:10,928 --> 00:08:12,949
+生产了很多之后
+
+193
+00:08:13,341 --> 00:08:16,293
+有几个样品当时还曾经送到了中国
+
+194
+00:08:17,077 --> 00:08:18,221
+可惜中国当时也没有用
+
+195
+00:08:19,653 --> 00:08:22,086
+这是打开后里面的装置
+
+196
+00:08:22,621 --> 00:08:26,725
+通过齿轮的咬合来进行十进制的计算
+
+197
+00:08:27,565 --> 00:08:28,597
+这是帕斯卡
+
+198
+00:08:29,131 --> 00:08:32,051
+那么帕斯卡他所做的工作
+
+199
+00:08:32,763 --> 00:08:35,202
+应该说在那个年代超前的
+
+200
+00:08:35,538 --> 00:08:36,538
+也是一种非凡的
+
+201
+00:08:37,571 --> 00:08:39,702
+那么 在帕斯卡生命的最后几年
+
+202
+00:08:40,131 --> 00:08:44,779
+他专心地在写 总结自己的思想
+
+203
+00:08:45,251 --> 00:08:46,899
+他在他的书中写道
+
+204
+00:08:47,251 --> 00:08:51,493
+这种计算器所进行的工作比动物的行为
+
+205
+00:08:51,931 --> 00:08:54,179
+更接近于人类的思维
+
+206
+00:08:55,747 --> 00:08:57,611
+也就是说 他实际上
+
+207
+00:08:57,971 --> 00:09:00,499
+提出了一种非凡的想法
+
+208
+00:09:01,099 --> 00:09:01,995
+为了实现这样一个目的
+
+209
+00:09:02,371 --> 00:09:02,707
+是什么
+
+210
+00:09:03,075 --> 00:09:08,475
+就是利用纯粹的机械的装置
+
+211
+00:09:09,691 --> 00:09:14,771
+来代替我们人类的思考和记忆
+
+212
+00:09:15,779 --> 00:09:17,867
+那么这种想法在当时可以说
+
+213
+00:09:18,210 --> 00:09:21,194
+是一种非凡的创新 非凡的创举
+
+214
+00:09:22,369 --> 00:09:25,449
+但是非常可惜 他有这样非凡的创举
+
+215
+00:09:25,894 --> 00:09:30,705
+但是帕斯卡呢 在39岁的时候就去世了
+
+216
+00:09:31,519 --> 00:09:33,082
+英年早逝了 特别可惜
+
+217
+00:09:33,673 --> 00:09:34,929
+那么帕斯卡其实自己呢
+
+218
+00:09:35,361 --> 00:09:36,538
+也对自己有一个评价
+
+219
+00:09:36,945 --> 00:09:40,722
+他说 人好比是脆弱的芦苇
+
+220
+00:09:41,218 --> 00:09:44,210
+但是 他又是有思想的芦苇
+
+221
+00:09:45,227 --> 00:09:46,593
+其实这句话很有意思
+
+222
+00:09:47,002 --> 00:09:47,793
+我们现在回想
+
+223
+00:09:48,146 --> 00:09:51,490
+我们现在很多人比一个强壮的芦苇
+
+224
+00:09:51,969 --> 00:09:53,529
+不知道要强壮多少倍
+
+225
+00:09:54,073 --> 00:09:58,362
+但是最终也是像芦苇一样悄无声息
+
+226
+00:09:59,209 --> 00:09:59,833
+为什么呢
+
+227
+00:10:00,514 --> 00:10:02,905
+大家可以想一想帕斯卡的这句话
+
+228
+00:10:05,661 --> 00:10:07,242
+好 那么帕斯卡之后
+
+229
+00:10:07,977 --> 00:10:11,257
+有一位比帕斯卡小二十多岁的年轻人
+
+230
+00:10:11,906 --> 00:10:15,682
+那么 他被帕斯卡的想法深深的迷住了
+
+231
+00:10:16,130 --> 00:10:17,490
+他后来设计了一个
+
+232
+00:10:17,842 --> 00:10:19,826
+大约在帕斯卡计算机之后
+
+233
+00:10:20,137 --> 00:10:21,937
+我们推算在大约在半个世纪之后
+
+234
+00:10:22,441 --> 00:10:24,881
+他设计了一个乘法器
+
+235
+00:10:25,681 --> 00:10:26,905
+大家看这个乘法器
+
+236
+00:10:27,450 --> 00:10:29,505
+那么这个乘法器看这个样子就比较
+
+237
+00:10:29,834 --> 00:10:31,697
+高端 大气 上档次
+
+238
+00:10:32,393 --> 00:10:33,337
+比之前的要好看多了
+
+239
+00:10:33,889 --> 00:10:35,298
+那么这个乘法器谁设计的呢
+
+240
+00:10:35,713 --> 00:10:37,225
+我们认识一下这位年轻人
+
+241
+00:10:38,338 --> 00:10:40,441
+他的名字叫莱布尼茨
+
+242
+00:10:42,233 --> 00:10:44,058
+那么这个莱布尼茨 他这个乘法器
+
+243
+00:10:44,681 --> 00:10:47,297
+和之前帕斯卡的加法器的区别在什么地方
+
+244
+00:10:47,834 --> 00:10:51,505
+它是二进制的 所以这是它最大的特点
+
+245
+00:10:51,833 --> 00:10:52,593
+二进制 乘法器
+
+246
+00:10:52,928 --> 00:10:53,673
+机械式的
+
+247
+00:10:54,073 --> 00:10:55,258
+那么说到这个二进制呢
+
+248
+00:10:55,505 --> 00:10:59,654
+应该说还是和中国的文化还是相当有些渊源
+
+249
+00:11:00,050 --> 00:11:02,825
+那么据说莱布尼茨的二进制想法
+
+250
+00:11:03,356 --> 00:11:05,418
+来自于我们的伏羲八卦图
+
+251
+00:11:06,114 --> 00:11:07,241
+那么怎么对应呢
+
+252
+00:11:07,697 --> 00:11:08,329
+就是伏羲八卦图当中的
+
+253
+00:11:08,737 --> 00:11:11,657
+乾 坤 坎 离 巽 艮 震 兑
+
+254
+00:11:11,938 --> 00:11:13,778
+这八个卦呢 分别可以用
+
+255
+00:11:14,057 --> 00:11:18,113
+二进制的000 001等等 这样表示出来
+
+256
+00:11:18,961 --> 00:11:20,540
+所以呢 有一种说法呢 说
+
+257
+00:11:20,915 --> 00:11:23,543
+莱布尼茨的二进制思想来源于中国的八卦
+
+258
+00:11:23,969 --> 00:11:25,393
+当然 莱布尼茨本人是否认的
+
+259
+00:11:25,905 --> 00:11:27,730
+但是又有中国学者又考证过
+
+260
+00:11:28,034 --> 00:11:29,090
+说他否认不了这一点
+
+261
+00:11:29,298 --> 00:11:30,761
+他肯定是之前见过这个八卦的
+
+262
+00:11:31,465 --> 00:11:33,906
+当然这个事情 我们不去再考证
+
+263
+00:11:34,385 --> 00:11:38,217
+它到底是起源于什么地方 二进制
+
+264
+00:11:38,489 --> 00:11:40,818
+我觉得我们现在自强是更重要的
+
+265
+00:11:42,161 --> 00:11:43,673
+好 那么我们这个小节呢
+
+266
+00:11:44,105 --> 00:11:47,161
+主要就给大家介绍了最远古的手动计算工具
+
+267
+00:11:47,521 --> 00:11:48,969
+和机械式的一些计算工具
+
+268
+00:11:49,489 --> 00:11:51,193
+那么也体现了先辈的智慧
+
+269
+00:11:52,465 --> 00:11:57,954
+但是真正对未来的 也就是现在的计算机科学
+
+270
+00:11:58,275 --> 00:11:59,841
+产生重大影响
+
+271
+00:12:00,241 --> 00:12:01,773
+我们有两位伟大的先驱
+
+272
+00:12:02,777 --> 00:12:03,657
+那么 他们的故事
+
+273
+00:12:03,929 --> 00:12:05,988
+我们在下一节将要给大家讲述
+
diff --git a/common/test/data/uploads/subs_t__eq_exist.srt.sjson b/common/test/data/uploads/subs_t__eq_exist.srt.sjson
new file mode 100644
index 000000000000..a016152f3b63
--- /dev/null
+++ b/common/test/data/uploads/subs_t__eq_exist.srt.sjson
@@ -0,0 +1,11 @@
+{
+ "start": [
+ 1000
+ ],
+ "end": [
+ 2000
+ ],
+ "text": [
+ "Equal transcripts"
+ ]
+}
\ No newline at end of file
diff --git a/common/test/data/uploads/subs_t_neq_exist.srt.sjson b/common/test/data/uploads/subs_t_neq_exist.srt.sjson
new file mode 100644
index 000000000000..3064722117b1
--- /dev/null
+++ b/common/test/data/uploads/subs_t_neq_exist.srt.sjson
@@ -0,0 +1,143 @@
+{
+ "start": [
+ 270,
+ 2720,
+ 5430,
+ 7160,
+ 10830,
+ 12880,
+ 15890,
+ 19000,
+ 22070,
+ 25170,
+ 27890,
+ 30590,
+ 32920,
+ 36360,
+ 39630,
+ 41170,
+ 42790,
+ 44590,
+ 47320,
+ 50250,
+ 51880,
+ 54320,
+ 57410,
+ 59160,
+ 62320,
+ 65099,
+ 68430,
+ 71360,
+ 73640,
+ 76580,
+ 78660,
+ 81480,
+ 83940,
+ 86230,
+ 88570,
+ 90520,
+ 93430,
+ 95940,
+ 99090,
+ 100910,
+ 103740,
+ 105610,
+ 108310,
+ 111100,
+ 112360
+ ],
+ "end": [
+ 2720,
+ 5430,
+ 7160,
+ 10830,
+ 12880,
+ 15890,
+ 19000,
+ 22070,
+ 25170,
+ 27890,
+ 30590,
+ 32920,
+ 36360,
+ 39630,
+ 41170,
+ 42790,
+ 44590,
+ 47320,
+ 50250,
+ 51880,
+ 54320,
+ 57410,
+ 59160,
+ 62320,
+ 65099,
+ 68430,
+ 71360,
+ 73640,
+ 76580,
+ 78660,
+ 81480,
+ 83940,
+ 86230,
+ 88570,
+ 90520,
+ 93430,
+ 95940,
+ 99090,
+ 100910,
+ 103740,
+ 105610,
+ 108310,
+ 111100,
+ 112360,
+ 114220
+ ],
+ "text": [
+ "LILA FISHER: Hi, welcome to Edx.",
+ "I'm Lila Fisher, an Edx fellow helping to put",
+ "together these courses.",
+ "As you know, our courses are entirely online.",
+ "So before we start learning about the subjects that",
+ "brought you here, let's learn about the tools that you will",
+ "use to navigate through the course material.",
+ "Let's start with what is on your screen right now.",
+ "You are watching a video of me talking.",
+ "You have several tools associated with these videos.",
+ "Some of them are standard video buttons, like the play",
+ "Pause Button on the bottom left.",
+ "Like most video players, you can see how far you are into",
+ "this particular video segment and how long the entire video",
+ "segment is.",
+ "Something that you might not be used to",
+ "is the speed option.",
+ "While you are going through the videos, you can speed up",
+ "or slow down the video player with these buttons.",
+ "Go ahead and try that now.",
+ "Make me talk faster and slower.",
+ "If you ever get frustrated by the pace of speech, you can",
+ "adjust it this way.",
+ "Another great feature is the transcript on the side.",
+ "This will follow along with everything that I am saying as",
+ "I am saying it, so you can read along if you like.",
+ "You can also click on any of the words, and you will notice",
+ "that the video jumps to that word.",
+ "The video slider at the bottom of the video will let you",
+ "navigate through the video quickly.",
+ "If you ever find the transcript distracting, you",
+ "can toggle the captioning button in order to make it go",
+ "away or reappear.",
+ "Now that you know about the video player, I want to point",
+ "out the sequence navigator.",
+ "Right now you're in a lecture sequence, which interweaves",
+ "many videos and practice exercises.",
+ "You can see how far you are in a particular sequence by",
+ "observing which tab you're on.",
+ "You can navigate directly to any video or exercise by",
+ "clicking on the appropriate tab.",
+ "You can also progress to the next element by pressing the",
+ "Arrow button, or by clicking on the next tab.",
+ "Try that now.",
+ "The tutorial will continue in the next video."
+ ]
+}
\ No newline at end of file
diff --git a/common/test/data/uploads/subs_t_not_exist.srt.sjson b/common/test/data/uploads/subs_t_not_exist.srt.sjson
new file mode 100644
index 000000000000..3064722117b1
--- /dev/null
+++ b/common/test/data/uploads/subs_t_not_exist.srt.sjson
@@ -0,0 +1,143 @@
+{
+ "start": [
+ 270,
+ 2720,
+ 5430,
+ 7160,
+ 10830,
+ 12880,
+ 15890,
+ 19000,
+ 22070,
+ 25170,
+ 27890,
+ 30590,
+ 32920,
+ 36360,
+ 39630,
+ 41170,
+ 42790,
+ 44590,
+ 47320,
+ 50250,
+ 51880,
+ 54320,
+ 57410,
+ 59160,
+ 62320,
+ 65099,
+ 68430,
+ 71360,
+ 73640,
+ 76580,
+ 78660,
+ 81480,
+ 83940,
+ 86230,
+ 88570,
+ 90520,
+ 93430,
+ 95940,
+ 99090,
+ 100910,
+ 103740,
+ 105610,
+ 108310,
+ 111100,
+ 112360
+ ],
+ "end": [
+ 2720,
+ 5430,
+ 7160,
+ 10830,
+ 12880,
+ 15890,
+ 19000,
+ 22070,
+ 25170,
+ 27890,
+ 30590,
+ 32920,
+ 36360,
+ 39630,
+ 41170,
+ 42790,
+ 44590,
+ 47320,
+ 50250,
+ 51880,
+ 54320,
+ 57410,
+ 59160,
+ 62320,
+ 65099,
+ 68430,
+ 71360,
+ 73640,
+ 76580,
+ 78660,
+ 81480,
+ 83940,
+ 86230,
+ 88570,
+ 90520,
+ 93430,
+ 95940,
+ 99090,
+ 100910,
+ 103740,
+ 105610,
+ 108310,
+ 111100,
+ 112360,
+ 114220
+ ],
+ "text": [
+ "LILA FISHER: Hi, welcome to Edx.",
+ "I'm Lila Fisher, an Edx fellow helping to put",
+ "together these courses.",
+ "As you know, our courses are entirely online.",
+ "So before we start learning about the subjects that",
+ "brought you here, let's learn about the tools that you will",
+ "use to navigate through the course material.",
+ "Let's start with what is on your screen right now.",
+ "You are watching a video of me talking.",
+ "You have several tools associated with these videos.",
+ "Some of them are standard video buttons, like the play",
+ "Pause Button on the bottom left.",
+ "Like most video players, you can see how far you are into",
+ "this particular video segment and how long the entire video",
+ "segment is.",
+ "Something that you might not be used to",
+ "is the speed option.",
+ "While you are going through the videos, you can speed up",
+ "or slow down the video player with these buttons.",
+ "Go ahead and try that now.",
+ "Make me talk faster and slower.",
+ "If you ever get frustrated by the pace of speech, you can",
+ "adjust it this way.",
+ "Another great feature is the transcript on the side.",
+ "This will follow along with everything that I am saying as",
+ "I am saying it, so you can read along if you like.",
+ "You can also click on any of the words, and you will notice",
+ "that the video jumps to that word.",
+ "The video slider at the bottom of the video will let you",
+ "navigate through the video quickly.",
+ "If you ever find the transcript distracting, you",
+ "can toggle the captioning button in order to make it go",
+ "away or reappear.",
+ "Now that you know about the video player, I want to point",
+ "out the sequence navigator.",
+ "Right now you're in a lecture sequence, which interweaves",
+ "many videos and practice exercises.",
+ "You can see how far you are in a particular sequence by",
+ "observing which tab you're on.",
+ "You can navigate directly to any video or exercise by",
+ "clicking on the appropriate tab.",
+ "You can also progress to the next element by pressing the",
+ "Arrow button, or by clicking on the next tab.",
+ "Try that now.",
+ "The tutorial will continue in the next video."
+ ]
+}
\ No newline at end of file
diff --git a/common/test/data/uploads/test_transcripts.srt b/common/test/data/uploads/test_transcripts.srt
new file mode 100644
index 000000000000..bdb165e5c63d
--- /dev/null
+++ b/common/test/data/uploads/test_transcripts.srt
@@ -0,0 +1,43 @@
+0
+00:00:00,270 --> 00:00:02,720
+LILA FISHER: Hi, welcome to Edx.
+
+1
+00:00:02,720 --> 00:00:05,430
+I'm Lila Fisher, an Edx fellow helping to put
+
+2
+00:00:05,430 --> 00:00:07,160
+together these courses.
+
+3
+00:00:07,160 --> 00:00:10,830
+As you know, our courses are entirely online.
+
+4
+00:00:10,830 --> 00:00:12,880
+So before we start learning about the subjects that
+
+5
+00:00:12,880 --> 00:00:15,890
+brought you here, let's learn about the tools that you will
+
+6
+00:00:15,890 --> 00:00:19,000
+use to navigate through the course material.
+
+7
+00:00:19,000 --> 00:00:22,070
+Let's start with what is on your screen right now.
+
+8
+00:00:22,070 --> 00:00:25,170
+You are watching a video of me talking.
+
+9
+00:00:25,170 --> 00:00:27,890
+You have several tools associated with these videos.
+
+10
+00:00:27,890 --> 00:00:30,590
+Some of them are standard video buttons, like the play
diff --git a/docs/developers/source/cms.rst b/docs/developers/source/cms.rst
index 11fa243f90c1..c09043d0b7c5 100644
--- a/docs/developers/source/cms.rst
+++ b/docs/developers/source/cms.rst
@@ -2,5 +2,9 @@
CMS module
*******************************************
+
.. module:: cms
+.. toctree::
+
+ transcripts.rst
diff --git a/docs/developers/source/transcripts.rst b/docs/developers/source/transcripts.rst
new file mode 100644
index 000000000000..8fb45b002495
--- /dev/null
+++ b/docs/developers/source/transcripts.rst
@@ -0,0 +1,256 @@
+.. module:: transcripts
+
+======================================================
+Developer’s workflow for the timed transcripts in CMS.
+======================================================
+
+:download:`Multipage pdf version of Timed Transcripts workflow.
`
+
+:download:`Open office graph version (source for pdf). `
+
+:download:`List of implemented acceptance tests. `
+
+
+Description
+===========
+
+Timed Transcripts functionality is added in separate tab of Video module Editor, that is active by default. This tab is called `Basic`, another tab is called `Advanced` and contains default metadata fields.
+
+`Basic` tab is a simple representation of `Advanced` tab that provides functionality to speed up adding Video module with transcripts to the course.
+
+To make more accurate adjustments `Advanced` tab should be used.
+
+Front-end part of `Basic` tab has 4 editors/views:
+ * Display name
+ * 3 editors for inserting Video URLs.
+
+Video URL fields might contain 3 kinds of URLs:
+ * **YouTube** link. There are supported formats:
+ * http://www.youtube.com/watch?v=OEoXaMPEzfM&feature=feedrec_grec_index ;
+ * http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/OEoXaMPEzfM ;
+ * http://www.youtube.com/v/OEoXaMPEzfM?fs=1&hl=en_US&rel=0 ;
+ * http://www.youtube.com/watch?v=OEoXaMPEzfM#t=0m10s ;
+ * http://www.youtube.com/embed/OEoXaMPEzfM?rel=0 ;
+ * http://www.youtube.com/watch?v=OEoXaMPEzfM ;
+ * http://youtu.be/OEoXaMPEzfM ;
+
+ * **MP4** video source;
+ * **WEBM** video source.
+
+Each of these kind of URLs can be specified just **ONCE**. Otherwise, error message occurs on front-end.
+
+After filling editor **transcripts/check** method will be invoked with the parameters described below (see `API`_). Depending on conditions, that are also described below (see `Commands`_), this method responds with a *command* and front-end renders the appropriate View.
+Each View can have specific actions. There is a list of supported actions:
+ * Download Timed Transcripts;
+ * Upload Timed Transcripts;
+ * Import Timed Transcripts from YouTube;
+ * Replace edX Timed Transcripts by Timed Transcripts from YouTube;
+ * Choose Timed Transcripts;
+ * Use existing Timed Transcripts.
+
+All of these actions are handled by 7 API methods described below (see `API`_).
+
+Because rollback functionality isn't implemented now, after invoking some of the actions user cannot revert changes by clicking button `Cancel`.
+
+To remove timed transcripts file from the video just go to `Advanced` tab and clear field `sub` then Save changes.
+
+
+Commands
+========
+
+Command from front-end point of view is just a reference to the needed View with possible actions that user can do depending on conditions described below (See edx-platform/cms/static/js/views/transcripts/message_manager.js:21-29).
+
+So,
+ * **IF** YouTube transcripts present locally **AND** on YouTube server **AND** both of these transcripts files are **DIFFERENT**, we respond with `replace` command. Ask user to replace local transcripts file by YouTube's ones.
+ * **IF** YouTube transcripts present **ONLY** locally, we respond with `found` command.
+ * **IF** YouTube transcripts present **ONLY** on YouTube server, we respond with `import` command. Ask user to import transcripts file from YouTube server.
+ * **IF** player is in HTML5 video mode. It means that **ONLY** html5 sources are added:
+ * **IF** just 1 html5 source was added or both html5 sources have **EQUAL** transcripts files, then we respond with `found` command.
+ * **OTHERWISE**, when 2 html5 sources were added and founded transcripts files are **DIFFERENT**, we respond with `choose` command. In this case, user should choose which one transcripts file he wants to use.
+ * **IF** we are working with just 1 field **AND** item.sub field **HAS** a value **AND** user fills editor/view by the new value/video source without transcripts file, we respond with `use_existing` command. In this case, user will have possibility to use transcripts file from previous video.
+ * **OTHERWISE**, we will respond with `not_found` command.
+
+
+Synchronization and Saving workflow
+====================================
+
+
+For now saving mechanism works as follows:
+
+On click `Save` button **ModuleEdit** class (See edx-platform/cms/static/coffee/src/views/module_edit.coffee:83-101) grabs values from all modified metadata fields and sends all this data to the server.
+
+Because of the fact that Timed Transcripts is module specific functionality, ModuleEdit class is not extended. Instead, to apply all changes that user did in the `Basic` tab, we use synchronization mechanism of TabsEditingDescriptor class. That mechanism provides us possibility to do needed actions on Tab switching and on Save (See edx-platform/cms/templates/widgets/video/transcripts.html).
+
+On tab switching and when save action is invoked, JavaScript code synchronize collections (Metadata Collection and Transcripts Collection). You can see synchronization logic in the edx-platform/cms/static/js/views/transcripts/editor.js:72-219. In this case, Metadata fields always have the actual data.
+
+
+Special cases
+=============
+
+1. Status message `Timed Transcript Conflict` (Choose) where one of 2 transcripts files should be chosen **-->** click `Save` button without choosing **-->** open Editor **-->** status message `Timed Transcript Found` will be shown and transcripts file will be chosen in random order.
+
+2. status message `Timed Transcript Conflict` (Choose) where one of 2 transcripts files should be chosen **-->** open `Advanced` tab without choosing **-->** get back to `Basic` tab **-->** status message `Timed Transcript Found` will be shown and transcripts file will be chosen in random order.
+
+3. The same issues with `Timed Transcript Not Updated` (Use existing).
+
+API
+===
+
+We provide 7 API methods to work with timed transcripts
+(edx-platform/cms/urls.py:23-29):
+ * transcripts/upload
+ * transcripts/download
+ * transcripts/check
+ * transcripts/choose
+ * transcripts/replace
+ * transcripts/rename
+ * transcripts/save
+
+**"transcripts/upload"** method is used for uploading SRT transcripts for the
+HTML5 and YouTube video modules.
+
+*Method:*
+ POST
+*Parameters:*
+ - id - location ID of the Xmodule
+ - video_list - list with information about the links currently passed in the editor/view.
+ - file - BLOB file
+*Response:*
+ HTTP 400
+ or
+ HTTP 200 + JSON:
+ .. code::
+ {
+ status: 'Success' or 'Error',
+ subs: value of uploaded and saved sub field in the video item.
+ }
+
+
+**"transcripts/download"** method is used for downloading SRT transcripts for the
+HTML5 and YouTube video modules.
+
+*Method:*
+ GET
+*Parameters:*
+ - id - location ID of the Xmodule
+ - subs_id - file name that is used to find transcripts file in the storage.
+*Response:*
+ HTTP 404
+ or
+ HTTP 200 + BLOB of SRT file
+
+
+**"transcripts/check"** method is used for checking availability of timed transcripts
+for the video module.
+
+*Method:*
+ GET
+*Parameters:*
+ - id - location ID of the Xmodule
+*Response:*
+ HTTP 400
+ or
+ HTTP 200 + JSON:
+ .. code::
+ {
+ command: string with action to front-end what to do and what to show to user,
+ subs: file name of transcripts file that was found in the storage,
+ html5_local: [] or [True] or [True, True],
+ is_youtube_mode: True/False,
+ youtube_local: True/False,
+ youtube_server: True/False,
+ youtube_diff: True/False,
+ current_item_subs: string with value of item.sub field,
+ status: 'Error' or 'Success'
+ }
+
+
+**"transcripts/choose"** method is used for choosing which transcripts file should be used.
+
+*Method:*
+ GET
+*Parameters:*
+ - id - location ID of the Xmodule
+ - video_list - list with information about the links currently passed in the editor/view.
+ - html5_id - file name of chosen transcripts file.
+
+*Response:*
+ HTTP 200 + JSON:
+ .. code::
+ {
+ status: 'Success' or 'Error',
+ subs: value of uploaded and saved sub field in the video item.
+ }
+
+
+**"transcripts/replace"** method is used for handling `import` and `replace` commands.
+Invoking this method starts downloading new transcripts file from YouTube server.
+
+*Method:*
+ GET
+*Parameters:*
+ - id - location ID of the Xmodule
+ - video_list - list with information about the links currently passed in the editor/view.
+
+*Response:*
+ HTTP 400
+ or
+ HTTP 200 + JSON:
+ .. code::
+ {
+ status: 'Success' or 'Error',
+ subs: value of uploaded and saved sub field in the video item.
+ }
+
+
+**"transcripts/rename"** method is used for handling `use_existing` command.
+After invoking this method current transcripts file will be copied and renamed to another one with name of current video passed in the editor/view.
+
+*Method:*
+ GET
+*Parameters:*
+ - id - location ID of the Xmodule
+ - video_list - list with information about the links currently passed in the editor/view.
+
+*Response:*
+ HTTP 400
+ or
+ HTTP 200 + JSON:
+ .. code::
+ {
+ status: 'Success' or 'Error',
+ subs: value of uploaded and saved sub field in the video item.
+ }
+
+
+**"transcripts/save"** method is used for handling `save` command.
+After invoking this method all changes will be saved that were done before this moment.
+
+*Method:*
+ GET
+*Parameters:*
+ - id - location ID of the Xmodule
+ - metadata - new values for the metadata fields.
+ - currents_subs - list with the file names of videos passed in the editor/view.
+
+*Response:*
+ HTTP 400
+ or
+ HTTP 200 + JSON:
+ .. code::
+ {
+ status: 'Success' or 'Error'
+ }
+
+
+Transcripts modules:
+====================
+
+.. automodule:: contentstore.views.transcripts_ajax
+ :members:
+ :show-inheritance:
+
+.. automodule:: contentstore.transcripts_utils
+ :members:
+ :show-inheritance:
+
diff --git a/docs/developers/source/transcripts_acceptance_tests.odt b/docs/developers/source/transcripts_acceptance_tests.odt
new file mode 100644
index 000000000000..268a5769454e
Binary files /dev/null and b/docs/developers/source/transcripts_acceptance_tests.odt differ
diff --git a/docs/developers/source/transcripts_workflow.odg b/docs/developers/source/transcripts_workflow.odg
new file mode 100644
index 000000000000..bf048fc7a541
Binary files /dev/null and b/docs/developers/source/transcripts_workflow.odg differ
diff --git a/docs/developers/source/transcripts_workflow.pdf b/docs/developers/source/transcripts_workflow.pdf
new file mode 100644
index 000000000000..1445ed18f0ab
Binary files /dev/null and b/docs/developers/source/transcripts_workflow.pdf differ
diff --git a/lms/djangoapps/courseware/features/youtube_setup.py b/lms/djangoapps/courseware/features/youtube_setup.py
index 8233d1f4586e..b9e536c5fe40 100644
--- a/lms/djangoapps/courseware/features/youtube_setup.py
+++ b/lms/djangoapps/courseware/features/youtube_setup.py
@@ -1,7 +1,6 @@
#pylint: disable=C0111
#pylint: disable=W0621
-
-from courseware.mock_youtube_server.mock_youtube_server import MockYoutubeServer
+from xmodule.util.mock_youtube_server.mock_youtube_server import MockYoutubeServer
from lettuce import before, after, world
from django.conf import settings
import threading
@@ -25,6 +24,8 @@ def setup_mock_youtube_server():
server.time_to_response = 1 # seconds
+ server.address = address
+
# Start the server running in a separate daemon thread
# Because the thread is a daemon, it will terminate
# when the main thread terminates.
diff --git a/lms/djangoapps/courseware/mock_youtube_server/mock_youtube_server.py b/lms/djangoapps/courseware/mock_youtube_server/mock_youtube_server.py
deleted file mode 100644
index 16ab36b6f799..000000000000
--- a/lms/djangoapps/courseware/mock_youtube_server/mock_youtube_server.py
+++ /dev/null
@@ -1,80 +0,0 @@
-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):
- self._send_head()
-
- def do_GET(self):
- '''
- Handle a GET request from the client and sends response back.
- '''
- self._send_head()
-
- logger.debug("Youtube provider received GET request to path {}".format(
- self.path)
- ) # Log the request
-
- 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_response(status_message)
-
- def _send_head(self):
- '''
- Send the response code and MIME headers
- '''
- self.send_response(200)
- self.send_header('Content-type', 'text/html')
- self.end_headers()
-
- def _send_response(self, message):
- '''
- Send message back to the client
- '''
- 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()
diff --git a/lms/djangoapps/courseware/mock_youtube_server/test_mock_youtube_server.py b/lms/djangoapps/courseware/mock_youtube_server/test_mock_youtube_server.py
deleted file mode 100644
index 4ccd7cdc58db..000000000000
--- a/lms/djangoapps/courseware/mock_youtube_server/test_mock_youtube_server.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""
-Test for Mock_Youtube_Server
-"""
-import unittest
-import threading
-import urllib
-from mock_youtube_server import MockYoutubeServer
-
-from nose.plugins.skip import SkipTest
-
-
-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):
-
- # This is a test of the test setup,
- # so it does not need to run as part of the unit test suite
- # You can re-enable it by commenting out the line below
- raise SkipTest
-
- # 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
- response_handle = urllib.urlopen(
- 'http://127.0.0.1:8034/feeds/api/videos/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func',
- )
- response = response_handle.read()
- self.assertEqual("""callback_func({"message": "I\'m youtube."})""", response)
diff --git a/rakelib/docs.rake b/rakelib/docs.rake
index 025bee372776..1617efa8afc9 100644
--- a/rakelib/docs.rake
+++ b/rakelib/docs.rake
@@ -5,7 +5,7 @@ desc "Invoke sphinx 'make build' to generate docs."
task :builddocs, [:type, :quiet] do |t, args|
args.with_defaults(:quiet => "quiet")
if args.type == 'dev'
- path = "docs/developer"
+ path = "docs/developers"
elsif args.type == 'author'
path = "docs/course_authors"
elsif args.type == 'data'
@@ -26,7 +26,7 @@ end
desc "Show docs in browser (mac and ubuntu)."
task :showdocs, [:options] do |t, args|
if args.options == 'dev'
- path = "docs/developer"
+ path = "docs/developers"
elsif args.options == 'author'
path = "docs/course_authors"
elsif args.options == 'data'
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 6887bd543269..211eb15de591 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -56,6 +56,7 @@ pyparsing==1.5.6
python-memcached==1.48
python-openid==2.2.5
pytz==2012h
+pysrt==0.4.7
PyYAML==3.10
requests==1.2.3
scipy==0.11.0