Skip to content

Commit

Permalink
Merge pull request #6 from gabor-boros/gabor/add-srt-download
Browse files Browse the repository at this point in the history
[SE-4161] Add Download Transcripts button
  • Loading branch information
OmarIthawi authored Mar 17, 2021
2 parents fb955a8 + 4a31836 commit 970385c
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 15 deletions.
10 changes: 8 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
language: python
python:
- "3.5"
- "3.6"

script: nosetests
env:
- PYTHONPATH=`pwd`

install:
- pip install -r requirements-test.txt

script: DJANGO_SETTINGS_MODULE=test_settings pytest wistiavideo
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ Click the *Edit* button to open up a form where you can enter module title and a

## Running Tests

Before running tests, run `pip install -r requirements-test.txt`.

```bash
nosetests wistiavideo
DJANGO_SETTINGS_MODULE=test_settings pytest wistiavideo
```

## License
Expand Down
Binary file modified doc/img/wistia_video_edit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-r requirements.txt

django-pyfs==3.0
mock==4.0.3
pytest-django==4.1.0
pytest==6.2.2
xblock-sdk==0.2.2
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ XBlock~=1.3.1
xblock-utils~=2.1.1
django==2.2.15
mako==1.0.2
requests==2.25.1
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ def package_data(pkg, roots):

setup(
name='wistiavideo-xblock',
version='0.2',
version='0.3',
description='wistiavideo XBlock', # TODO: write a better description.
license='GPL v3',
packages=[
'wistiavideo',
],
install_requires=[
'XBlock',
'xblock-utils'
'xblock-utils',
'requests',
],
entry_points={
'xblock.v1': [
Expand Down
7 changes: 7 additions & 0 deletions test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from workbench.settings import *
from django.conf.global_settings import LOGGING

DEBUG = True

INSTALLED_APPS += ('wistiavideo', )
SECRET_KEY = '4l0ngs3cr3tstr1ngw3lln0ts0l0ngw41tn0w1tsl0ng3n0ugh'
23 changes: 23 additions & 0 deletions wistiavideo/static/css/wistiavideo.css
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
/* CSS for WistiaVideoXBlock */

.wistiavideo_block .wistia_responsive_transcripts {
margin: 1rem 0;
}

.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download {
background: rgba(30, 123, 237, 0.9);
border-radius: 0.1875rem;
padding: 11px 18px 11px 16px;
}

.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download,
.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download:hover,
.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download:active,
.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download:visited {
color: white;
}

.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download:hover,
.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download:active,
.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download:visited {
background: rgba(30, 123, 237, 1);
}
7 changes: 5 additions & 2 deletions wistiavideo/static/html/wistiavideo.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<div class="wistiavideo_block">
<script src="//fast.wistia.com/embed/medias/{self.media_id}.jsonp" async></script>
<script src="//fast.wistia.com/embed/medias/{{ media_id }}.jsonp" async></script>
<script src="//fast.wistia.com/assets/external/E-v1.js" async></script>
<div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;">
<div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;">
<div class="wistia_embed wistia_async_{self.media_id} videoFoam=true" style="height:100%;width:100%">&nbsp;</div>
<div id="wistia_{{ media_id }}" data-media-id="{{ media_id }}" class="wistia_embed wistia_async_{{ media_id }} videoFoam=true" style="height:100%;width:100%">&nbsp;</div>
</div>
</div>
<p class="wistia_responsive_transcripts">
<a href="#" style="display: none;" class="wistia_transcripts_download">{{ download_transcripts_text }}</a>
</p>
</div>
23 changes: 23 additions & 0 deletions wistiavideo/static/js/wistiavideo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function WistiaVideoXBlock(runtime, element) {
"use strict";

const downloadHandlerUrl = runtime.handlerUrl(element, 'download_captions');
const downloadBtnSelector = '.wistiavideo_block .wistia_responsive_transcripts .wistia_transcripts_download';
const wistiaEmbeddedSelector = ".wistiavideo_block .wistia_responsive_padding .wistia_responsive_wrapper .wistia_embed";
const mediaId = $(wistiaEmbeddedSelector).data("mediaId");

$(downloadBtnSelector, element).click(function (e) {
e.preventDefault();
window.open(downloadHandlerUrl, '_blank');
});

window._wq = window._wq || [];

_wq.push({ id: mediaId, onReady: function(video) {
video.plugin('captions').then(function (captions) {
if (captions.captions.length > 0) {
$(downloadBtnSelector).show();
}
});
}});
}
57 changes: 53 additions & 4 deletions wistiavideo/tests/test_xblock.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from mock import Mock, patch
import json
import unittest

from mock import Mock, patch

from xblock.runtime import KvsFieldData, DictKeyValueStore
from xblock.test.tools import (
TestRuntime
Expand All @@ -10,18 +12,21 @@


class WistiaXblockBaseTests(object):
def make_xblock(self):
def make_xblock(self, **kwargs):
key_store = DictKeyValueStore()
field_data = KvsFieldData(key_store)
runtime = TestRuntime(services={'field-data': field_data})
xblock = WistiaVideoXBlock(runtime, scope_ids=Mock())

for attr, val in kwargs.items():
setattr(xblock, attr, val)

return xblock


class WistiaXblockTests(WistiaXblockBaseTests, unittest.TestCase):
def test_media_id_property(self):
xblock = self.make_xblock()
xblock.href = 'https://example.wistia.com/medias/12345abcde'
xblock = self.make_xblock(href='https://example.wistia.com/medias/12345abcde')
self.assertEquals(xblock.media_id, '12345abcde')

def test_student_view(self):
Expand All @@ -30,6 +35,50 @@ def test_student_view(self):
student_view_html = xblock.student_view()
self.assertIn(xblock.media_id, student_view_html.body_html())

class WistiaXblockTranscriptsDownloadTests(WistiaXblockBaseTests, unittest.TestCase):
def __render_html(self):
xblock = self.make_xblock()
return xblock.student_view().body_html()

def test_transcripts_block_exists(self):
self.assertIn("wistia_responsive_transcripts", self.__render_html())

def test_download_button_exists(self):
self.assertIn("wistia_transcripts_download", self.__render_html())

@patch("wistiavideo.wistiavideo.requests")
def test_send_request(self, mock_requests):
media_id = "12345abcde"
href = "https://example.wistia.com/medias/{}".format(media_id)
expected_token = "token"
expected_url = "https://api.wistia.com/v1/medias/{}/captions.json".format(media_id)

mock_requests.get.return_value = Mock(json=Mock(return_value=[
{
"language": "eng",
"text": "caption text",
},
]))

xblock = self.make_xblock(
href=href,
access_token=expected_token
)

response = xblock.download_captions(Mock())

self.assertEqual(response.status, "200 OK")
self.assertNotEqual(response.body, b"")
self.assertEqual(response.headers["Content-Type"], "application/zip; charset=UTF-8")
self.assertEqual(
response.headers["Content-Disposition"],
"attachment; filename=captions_{}.zip".format(media_id)
)

mock_requests.get.assert_called_once_with(
expected_url,
params={"access_token": expected_token}
)

class WistiaXblockValidationTests(WistiaXblockBaseTests, unittest.TestCase):
def test_validate_correct_inputs(self):
Expand Down
117 changes: 113 additions & 4 deletions wistiavideo/wistiavideo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,124 @@
All you need to provide is video url, this XBlock doest the rest for you.
"""

import os
import pkg_resources
import re
import tempfile

import requests

from zipfile import ZipFile, ZIP_DEFLATED
from pathlib import Path

from xblock.core import XBlock
from xblock.fields import Scope, Integer, String
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage

from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from webob import Response

_ = lambda text: text

loader = ResourceLoader(__name__)
# From official Wistia documentation. May change in the future
# https://wistia.com/doc/construct-an-embed-code#the_regex
VIDEO_URL_RE = re.compile(r'https?:\/\/(.+)?(wistia.com|wi.st)\/(medias|embed)\/.*')


class WistiaVideoXBlock(StudioEditableXBlockMixin, XBlock):
class CaptionDownloadMixin:
"""
Mixin providing utility functions and handler to download captions from Wistia.
The utility mixin is heavily depending on the media ID property provided by the XBlock.
"""

access_token = String(
default='',
display_name=_('Wistia API key'),
help=_('The API key related to the account where the video uploaded to.'),
scope=Scope.content,
)

caption_download_editable_fields = ('access_token',)

def __send_request(self, url):
"""
Send a request to Wistia API using the given access token if provided and return the
response as a parsed JSON string.
"""

return requests.get(url, params={"access_token": self.access_token}).json()

@staticmethod
def __compress_captions(srt_files):
"""
Compress files into a zip file.
"""

zip_file = tempfile.NamedTemporaryFile(
prefix="captions_",
suffix=".zip",
delete=False
)

with ZipFile(zip_file.name, mode="w", compression=ZIP_DEFLATED) as compressed:
for srt_file in srt_files:
compressed.write(srt_file, srt_file.split("/")[-1])

return Path(zip_file.name)

@staticmethod
def __save_caption(caption):
"""
Save the caption to a temporary file and return its absolute path.
"""

language = caption["language"]
content = caption['text']

srt_file = tempfile.NamedTemporaryFile(
prefix="{}_".format(language),
suffix=".srt",
delete=False
)

srt_file.write(bytes(content, encoding="UTF-8"))
srt_file.close()

return srt_file.name

@XBlock.handler
def download_captions(self, request, suffix=""):
"""
Handle captions download.
Get captions text available for the media ID and save them in separate files. The saved
captions will be compressed and prepared for download in the response.
"""

response = self.__send_request(
"https://api.wistia.com/v1/medias/{}/captions.json".format(self.media_id),
)

srt_files = map(self.__save_caption, response)
zip_file = self.__compress_captions(srt_files)
map(os.unlink, srt_files)

return Response(
body=zip_file.read_bytes(),
headerlist=[
("Content-Type", "application/zip; charset=UTF-8"),
("Content-Disposition", "attachment; filename=captions_{}.zip".format(
self.media_id,
)),
],
)


class WistiaVideoXBlock(StudioEditableXBlockMixin, CaptionDownloadMixin, XBlock):

display_name = String(
default='Wistia video',
Expand All @@ -38,6 +138,7 @@ class WistiaVideoXBlock(StudioEditableXBlockMixin, XBlock):
)

editable_fields = ('display_name', 'href')
editable_fields += CaptionDownloadMixin.caption_download_editable_fields

@property
def media_id(self):
Expand Down Expand Up @@ -66,9 +167,17 @@ def student_view(self, context=None):
The primary view of the WistiaVideoXBlock, shown to students
when viewing courses.
"""
html = self.resource_string('static/html/wistiavideo.html')
frag = Fragment(html.format(self=self))

context = {
"download_transcripts_text": _("Download transcripts"),
"media_id": self.media_id,
}

frag = Fragment(loader.render_template('static/html/wistiavideo.html', context))
frag.add_css(self.resource_string('static/css/wistiavideo.css'))
frag.add_javascript(loader.load_unicode('static/js/wistiavideo.js'))
frag.initialize_js('WistiaVideoXBlock', {})

return frag

@staticmethod
Expand Down

0 comments on commit 970385c

Please sign in to comment.