Merge remote-tracking branch 'edx/master' into opaque-keys

Conflicts:
	cms/djangoapps/contentstore/tests/test_contentstore.py
	cms/djangoapps/contentstore/views/component.py
	cms/djangoapps/contentstore/views/item.py
	cms/djangoapps/contentstore/views/preview.py
	cms/djangoapps/contentstore/views/tests/test_container.py
	cms/static/js/spec/views/unit_spec.js
	cms/static/js/utils/module.js
	cms/templates/container.html
	cms/templates/studio_vertical_wrapper.html
	cms/templates/studio_xblock_wrapper.html
	common/djangoapps/student/views.py
	lms/templates/notes.html
	lms/templates/textannotation.html
	lms/templates/videoannotation.html
This commit is contained in:
Calen Pennington
2014-05-22 10:52:00 -04:00
178 changed files with 4916 additions and 3629 deletions

View File

@@ -0,0 +1,32 @@
"""
This file contains a function used to retrieve the token for the annotation backend
without having to create a view, but just returning a string instead.
It can be called from other files by using the following:
from xmodule.annotator_token import retrieve_token
"""
import datetime
from firebase_token_generator import create_token
def retrieve_token(userid, secret):
'''
Return a token for the backend of annotations.
It uses the course id to retrieve a variable that contains the secret
token found in inheritance.py. It also contains information of when
the token was issued. This will be stored with the user along with
the id for identification purposes in the backend.
'''
# the following five lines of code allows you to include the default timezone in the iso format
# for more information: http://stackoverflow.com/questions/3401428/how-to-get-an-isoformat-datetime-string-including-the-default-timezone
dtnow = datetime.datetime.now()
dtutcnow = datetime.datetime.utcnow()
delta = dtnow - dtutcnow
newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60)
newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin)
# uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a
# federated system in the annotation backend server
custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400}
newtoken = create_token(secret, custom_data)
return newtoken

View File

@@ -301,6 +301,7 @@ function (VideoPlayer, VideoStorage) {
// TODO: use 1 class to work with.
state.el.find('.video-player div').addClass('hidden');
state.el.find('.video-player h3').removeClass('hidden');
_hideWaitPlaceholder(state);
console.log(
'[Video info]: Non-youtube video sources aren\'t available.'
@@ -673,6 +674,15 @@ function (VideoPlayer, VideoStorage) {
}
}
});
// None of the supported video formats can be played. Hide the spinner.
if (!(_.compact(_.values(this.html5Sources)))) {
_hideWaitPlaceholder(state);
console.log(
'[Video info]: This browser cannot play .mp4, .ogg, or .webm ' +
'files'
);
}
}
// function fetchMetadata()

View File

@@ -216,7 +216,10 @@ function () {
// video element via jquery (http://bugs.jquery.com/ticket/9174) we
// create it using native JS.
this.video = document.createElement('video');
this.video.innerHTML = _.values(sourceStr).join('');
errorMessage = gettext('This browser cannot play .mp4, .ogg, or ' +
'.webm files. Try using a different browser, such as Google ' +
'Chrome.');
this.video.innerHTML = _.values(sourceStr).join('') + errorMessage;
// Get the jQuery object, and set the player state to UNSTARTED.
// The player state is used by other parts of the VideoPlayer to

View File

@@ -0,0 +1,30 @@
"""
Mixin to support editing in Studio.
"""
class StudioEditableModule(object):
"""
Helper methods for supporting Studio editing of xblocks.
"""
def render_reorderable_children(self, context, fragment):
"""
Renders children with the appropriate HTML structure for drag and drop.
"""
contents = []
for child in self.get_display_items():
context['reorderable_items'].add(child.location)
rendered_child = child.render('student_view', context)
fragment.add_frag_resources(rendered_child)
contents.append({
'id': child.id,
'content': rendered_child.content
})
fragment.add_content(self.system.render_template("studio_render_children_view.html", {
'items': contents,
'xblock_context': context,
}))

View File

@@ -0,0 +1,20 @@
"""
This test will run for annotator_token.py
"""
import unittest
from xmodule.annotator_token import retrieve_token
class TokenRetriever(unittest.TestCase):
"""
Tests to make sure that when passed in a username and secret token, that it will be encoded correctly
"""
def test_token(self):
"""
Test for the token generator. Give an a random username and secret token, it should create the properly encoded string of text.
"""
expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAyLTI3VDE3OjAwOjQyLjQwNjQ0MSswOjAwIiwgImNvbnN1bWVyS2V5IjogImZha2Vfc2VjcmV0IiwgInVzZXJJZCI6ICJ1c2VybmFtZSIsICJ0dGwiOiA4NjQwMH0.Dx1PoF-7mqBOOSGDMZ9R_s3oaaLRPnn6CJgGGF2A5CQ"
response = retrieve_token("username", "fake_secret")
self.assertEqual(expected.split('.')[0], response.split('.')[0])
self.assertNotEqual(expected.split('.')[2], response.split('.')[2])

View File

@@ -0,0 +1,25 @@
"""
Tests for StudioEditableModule.
"""
from xmodule.tests.test_vertical import BaseVerticalModuleTest
class StudioEditableModuleTestCase(BaseVerticalModuleTest):
def test_render_reorderable_children(self):
"""
Test the behavior of render_reorderable_children.
"""
reorderable_items = set()
context = {
'runtime_type': 'studio',
'container_view': True,
'reorderable_items': reorderable_items,
'read_only': False,
'root_xblock': self.vertical,
}
# Both children of the vertical should be rendered as reorderable
self.module_system.render(self.vertical, 'student_view', context).content
self.assertIn(self.vertical.get_children()[0].location, reorderable_items)
self.assertIn(self.vertical.get_children()[1].location, reorderable_items)

View File

@@ -38,17 +38,6 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
ScopeIds(None, None, None, None)
)
def test_render_content(self):
"""
Tests to make sure the sample xml is rendered and that it forms a valid xmltree
that does not contain a display_name.
"""
content = self.mod._render_content() # pylint: disable=W0212
self.assertIsNotNone(content)
element = etree.fromstring(content)
self.assertIsNotNone(element)
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
def test_extract_instructions(self):
"""
Tests to make sure that the instructions are correctly pulled from the sample xml above.
@@ -70,5 +59,5 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
Tests the function that passes in all the information in the context that will be used in templates/textannotation.html
"""
context = self.mod.get_html()
for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage']:
for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage', 'token']:
self.assertIn(key, context)

View File

@@ -0,0 +1,65 @@
"""
Tests for vertical module.
"""
from fs.memoryfs import MemoryFS
from xmodule.tests import get_test_system
from xmodule.tests.xml import XModuleXmlImportTest
from xmodule.tests.xml import factories as xml
class BaseVerticalModuleTest(XModuleXmlImportTest):
test_html_1 = 'Test HTML 1'
test_html_2 = 'Test HTML 2'
def setUp(self):
self.course_id = 'test_org/test_course_number/test_run'
# construct module
course = xml.CourseFactory.build()
sequence = xml.SequenceFactory.build(parent=course)
vertical = xml.VerticalFactory.build(parent=sequence)
self.course = self.process_xml(course)
xml.HtmlFactory(parent=vertical, url_name='test-html-1', text=self.test_html_1)
xml.HtmlFactory(parent=vertical, url_name='test-html-2', text=self.test_html_2)
self.course = self.process_xml(course)
course_seq = self.course.get_children()[0]
self.module_system = get_test_system()
def get_module(descriptor):
"""Mocks module_system get_module function"""
module_system = get_test_system()
module_system.get_module = get_module
descriptor.bind_for_student(module_system, descriptor._field_data) # pylint: disable=protected-access
return descriptor
self.module_system.get_module = get_module
self.module_system.descriptor_system = self.course.runtime
self.course.runtime.export_fs = MemoryFS()
self.vertical = course_seq.get_children()[0]
self.vertical.xmodule_runtime = self.module_system
class VerticalModuleTestCase(BaseVerticalModuleTest):
def test_render_student_view(self):
"""
Test the rendering of the student view.
"""
html = self.module_system.render(self.vertical, 'student_view', {}).content
self.assertIn(self.test_html_1, html)
self.assertIn(self.test_html_2, html)
def test_render_studio_view(self):
"""
Test the rendering of the Studio view
"""
reorderable_items = set()
context = {
'runtime_type': 'studio',
'reorderable_items': reorderable_items,
}
html = self.module_system.render(self.vertical, 'student_view', context).content
self.assertIn(self.test_html_1, html)
self.assertIn(self.test_html_2, html)

View File

@@ -34,100 +34,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
ScopeIds(None, None, None, None)
)
def test_annotation_class_attr_default(self):
"""
Makes sure that it can detect annotation values in text-form if user
decides to add text to the area below video, video functionality is completely
found in javascript.
"""
xml = '<annotation title="x" body="y" problem="0">test</annotation>'
element = etree.fromstring(xml)
expected_attr = {'class': {'value': 'annotatable-span highlight'}}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_valid_highlight(self):
"""
Same as above but more specific to an area that is highlightable in the appropriate
color designated.
"""
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for color in self.mod.highlight_colors:
element = etree.fromstring(xml.format(highlight=color))
value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color)
expected_attr = {'class': {
'value': value,
'_delete': 'highlight'}
}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_invalid_highlight(self):
"""
Same as above, but checked with invalid colors.
"""
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for invalid_color in ['rainbow', 'blink', 'invisible', '', None]:
element = etree.fromstring(xml.format(highlight=invalid_color))
expected_attr = {'class': {
'value': 'annotatable-span highlight',
'_delete': 'highlight'}
}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_data_attr(self):
"""
Test that each highlight contains the data information from the annotation itself.
"""
element = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
expected_attr = {
'data-comment-body': {'value': 'foo', '_delete': 'body'},
'data-comment-title': {'value': 'bar', '_delete': 'title'},
'data-problem-id': {'value': '0', '_delete': 'problem'}
}
actual_attr = self.mod._get_annotation_data_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_render_annotation(self):
"""
Tests to make sure that the spans designating annotations acutally visually render as annotations.
"""
expected_html = '<span class="annotatable-span highlight highlight-yellow" data-comment-title="x" data-comment-body="y" data-problem-id="0">z</span>'
expected_el = etree.fromstring(expected_html)
actual_el = etree.fromstring('<annotation title="x" body="y" problem="0" highlight="yellow">z</annotation>')
self.mod._render_annotation(actual_el) # pylint: disable=W0212
self.assertEqual(expected_el.tag, actual_el.tag)
self.assertEqual(expected_el.text, actual_el.text)
self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib))
def test_render_content(self):
"""
Like above, but using the entire text, it makes sure that display_name is removed and that there is only one
div encompassing the annotatable area.
"""
content = self.mod._render_content() # pylint: disable=W0212
element = etree.fromstring(content)
self.assertIsNotNone(element)
self.assertEqual('div', element.tag, 'root tag is a div')
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
def test_extract_instructions(self):
"""
This test ensures that if an instruction exists it is pulled and
@@ -160,6 +66,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
"""
Tests to make sure variables passed in truly exist within the html once it is all rendered.
"""
context = self.mod.get_html()
for key in ['display_name', 'content_html', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'alert', 'annotation_storage']:
context = self.mod.get_html() # pylint: disable=W0212
for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']:
self.assertIn(key, context)

View File

@@ -6,6 +6,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String
from xmodule.annotator_token import retrieve_token
import textwrap
@@ -30,7 +31,7 @@ class AnnotatableFields(object):
scope=Scope.settings,
default='Text Annotation',
)
tags = String(
instructor_tags = String(
display_name="Tags for Assignments",
help="Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue",
scope=Scope.settings,
@@ -43,6 +44,7 @@ class AnnotatableFields(object):
default='None',
)
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation")
class TextAnnotationModule(AnnotatableFields, XModule):
@@ -59,15 +61,9 @@ class TextAnnotationModule(AnnotatableFields, XModule):
self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode')
self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content)
if 'display_name' in xmltree.attrib:
del xmltree.attrib['display_name']
return etree.tostring(xmltree, encoding='unicode')
self.user_email = ""
if self.runtime.get_real_user is not None:
self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email
def _extract_instructions(self, xmltree):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
@@ -83,13 +79,13 @@ class TextAnnotationModule(AnnotatableFields, XModule):
context = {
'course_key': self.runtime.course_id,
'display_name': self.display_name_with_default,
'tag': self.tags,
'tag': self.instructor_tags,
'source': self.source,
'instructions_html': self.instructions,
'content_html': self._render_content(),
'annotation_storage': self.annotation_storage_url
'content_html': self.content,
'annotation_storage': self.annotation_storage_url,
'token': retrieve_token(self.user_email, self.annotation_token_secret),
}
return self.system.render_template('textannotation.html', context)
@@ -102,6 +98,7 @@ class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor):
def non_editable_metadata_fields(self):
non_editable_fields = super(TextAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
TextAnnotationDescriptor.annotation_storage_url
TextAnnotationDescriptor.annotation_storage_url,
TextAnnotationDescriptor.annotation_token_secret,
])
return non_editable_fields

View File

@@ -2,6 +2,7 @@ from xblock.fragment import Fragment
from xmodule.x_module import XModule
from xmodule.seq_module import SequenceDescriptor
from xmodule.progress import Progress
from xmodule.studio_editable import StudioEditableModule
from pkg_resources import resource_string
from copy import copy
@@ -14,7 +15,7 @@ class VerticalFields(object):
has_children = True
class VerticalModule(VerticalFields, XModule):
class VerticalModule(VerticalFields, XModule, StudioEditableModule):
''' Layout module for laying out submodules vertically.'''
def student_view(self, context):
@@ -28,7 +29,9 @@ class VerticalModule(VerticalFields, XModule):
"""
Renders the Studio preview view, which supports drag and drop.
"""
return self.render_view(context, 'vert_module_studio_view.html')
fragment = Fragment()
self.render_reorderable_children(context, fragment)
return fragment
def render_view(self, context, template_name):
"""

View File

@@ -44,6 +44,7 @@ def get_ext(filename):
log = logging.getLogger(__name__)
_ = lambda text: text
class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
@@ -177,12 +178,12 @@ class VideoDescriptor(VideoFields, VideoStudioViewHandlers, TabsEditingDescripto
tabs = [
{
'name': "Basic",
'name': _("Basic"),
'template': "video/transcripts.html",
'current': True
},
{
'name': "Advanced",
'name': _("Advanced"),
'template': "tabs/metadata-edit-tab.html"
}
]
@@ -358,7 +359,7 @@ class VideoDescriptor(VideoFields, VideoStudioViewHandlers, TabsEditingDescripto
_ = self.runtime.service(self, "i18n").ugettext
video_url.update({
'help': _('The URL for your video. This can be a YouTube URL or a link to an .mp4, .ogg, or .webm video file hosted elsewhere on the Internet.'),
'display_name': 'Default Video URL',
'display_name': _('Default Video URL'),
'field_name': 'video_url',
'type': 'VideoList',
'default_value': [get_youtube_link(youtube_id_1_0['default_value'])]

View File

@@ -14,51 +14,52 @@ _ = lambda text: text
class VideoFields(object):
"""Fields for `VideoModule` and `VideoDescriptor`."""
display_name = String(
display_name="Component Display Name", help="The name students see. This name appears in the course ribbon and as a header for the video.",
help=_("The name students see. This name appears in the course ribbon and as a header for the video."),
display_name=_("Component Display Name"),
default="Video",
scope=Scope.settings
)
saved_video_position = RelativeTime(
help="Current position in the video.",
help=_("Current position in the video."),
scope=Scope.user_state,
default=datetime.timedelta(seconds=0)
)
# TODO: This should be moved to Scope.content, but this will
# require data migration to support the old video module.
youtube_id_1_0 = String(
help="Optional, for older browsers: the YouTube ID for the normal speed video.",
display_name="YouTube ID",
help=_("Optional, for older browsers: the YouTube ID for the normal speed video."),
display_name=_("YouTube ID"),
scope=Scope.settings,
default="OEoXaMPEzfM"
)
youtube_id_0_75 = String(
help="Optional, for older browsers: the YouTube ID for the .75x speed video.",
display_name="YouTube ID for .75x speed",
help=_("Optional, for older browsers: the YouTube ID for the .75x speed video."),
display_name=_("YouTube ID for .75x speed"),
scope=Scope.settings,
default=""
)
youtube_id_1_25 = String(
help="Optional, for older browsers: the YouTube ID for the 1.25x speed video.",
display_name="YouTube ID for 1.25x speed",
help=_("Optional, for older browsers: the YouTube ID for the 1.25x speed video."),
display_name=_("YouTube ID for 1.25x speed"),
scope=Scope.settings,
default=""
)
youtube_id_1_5 = String(
help="Optional, for older browsers: the YouTube ID for the 1.5x speed video.",
display_name="YouTube ID for 1.5x speed",
help=_("Optional, for older browsers: the YouTube ID for the 1.5x speed video."),
display_name=_("YouTube ID for 1.5x speed"),
scope=Scope.settings,
default=""
)
start_time = RelativeTime( # datetime.timedelta object
help="Time you want the video to start if you don't want the entire video to play. Formatted as HH:MM:SS. The maximum value is 23:59:59.",
display_name="Video Start Time",
help=_("Time you want the video to start if you don't want the entire video to play. Formatted as HH:MM:SS. The maximum value is 23:59:59."),
display_name=_("Video Start Time"),
scope=Scope.settings,
default=datetime.timedelta(seconds=0)
)
end_time = RelativeTime( # datetime.timedelta object
help="Time you want the video to stop if you don't want the entire video to play. Formatted as HH:MM:SS. The maximum value is 23:59:59.",
display_name="Video Stop Time",
help=_("Time you want the video to stop if you don't want the entire video to play. Formatted as HH:MM:SS. The maximum value is 23:59:59."),
display_name=_("Video Stop Time"),
scope=Scope.settings,
default=datetime.timedelta(seconds=0)
)
@@ -67,61 +68,61 @@ class VideoFields(object):
# `source` is deprecated field and should not be used in future.
# `download_video` is used instead.
source = String(
help="The external URL to download the video.",
display_name="Download Video",
help=_("The external URL to download the video."),
display_name=_("Download Video"),
scope=Scope.settings,
default=""
)
download_video = Boolean(
help="Allow students to download versions of this video in different formats if they cannot use the edX video player or do not have access to YouTube. You must add at least one non-YouTube URL in the Video File URLs field.",
display_name="Video Download Allowed",
help=_("Allow students to download versions of this video in different formats if they cannot use the edX video player or do not have access to YouTube. You must add at least one non-YouTube URL in the Video File URLs field."),
display_name=_("Video Download Allowed"),
scope=Scope.settings,
default=False
)
html5_sources = List(
help="The URL or URLs where you've posted non-YouTube versions of the video. Each URL must end in .mpeg, .mp4, .ogg, or .webm and cannot be a YouTube URL. Students will be able to view the first listed video that's compatible with the student's computer. To allow students to download these videos, set Video Download Allowed to True.",
display_name="Video File URLs",
help=_("The URL or URLs where you've posted non-YouTube versions of the video. Each URL must end in .mpeg, .mp4, .ogg, or .webm and cannot be a YouTube URL. Students will be able to view the first listed video that's compatible with the student's computer. To allow students to download these videos, set Video Download Allowed to True."),
display_name=_("Video File URLs"),
scope=Scope.settings,
)
track = String(
help="By default, students can download an .srt or .txt transcript when you set Download Transcript Allowed to True. If you want to provide a downloadable transcript in a different format, we recommend that you upload a handout by using the Upload a Handout field. If this isn't possible, you can post a transcript file on the Files & Uploads page or on the Internet, and then add the URL for the transcript here. Students see a link to download that transcript below the video.",
display_name="Downloadable Transcript URL",
help=_("By default, students can download an .srt or .txt transcript when you set Download Transcript Allowed to True. If you want to provide a downloadable transcript in a different format, we recommend that you upload a handout by using the Upload a Handout field. If this isn't possible, you can post a transcript file on the Files & Uploads page or on the Internet, and then add the URL for the transcript here. Students see a link to download that transcript below the video."),
display_name=_("Downloadable Transcript URL"),
scope=Scope.settings,
default=''
)
download_track = Boolean(
help="Allow students to download the timed transcript. A link to download the file appears below the video. By default, the transcript is an .srt or .txt file. If you want to provide the transcript for download in a different format, upload a file by using the Upload Handout field.",
display_name="Download Transcript Allowed",
help=_("Allow students to download the timed transcript. A link to download the file appears below the video. By default, the transcript is an .srt or .txt file. If you want to provide the transcript for download in a different format, upload a file by using the Upload Handout field."),
display_name=_("Download Transcript Allowed"),
scope=Scope.settings,
default=False
)
sub = String(
help="The default transcript for the video, from the Default Timed Transcript field on the Basic tab. This transcript should be in English. You don't have to change this setting.",
display_name="Default Timed Transcript",
help=_("The default transcript for the video, from the Default Timed Transcript field on the Basic tab. This transcript should be in English. You don't have to change this setting."),
display_name=_("Default Timed Transcript"),
scope=Scope.settings,
default=""
)
show_captions = Boolean(
help="Specify whether the transcripts appear with the video by default.",
display_name="Show Transcript",
help=_("Specify whether the transcripts appear with the video by default."),
display_name=_("Show Transcript"),
scope=Scope.settings,
default=True
)
# Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
transcripts = Dict(
help="Add transcripts in different languages. Click below to specify a language and upload an .srt transcript file for that language.",
display_name="Transcript Languages",
help=_("Add transcripts in different languages. Click below to specify a language and upload an .srt transcript file for that language."),
display_name=_("Transcript Languages"),
scope=Scope.settings,
default={}
)
transcript_language = String(
help="Preferred language for transcript.",
display_name="Preferred language for transcript",
help=_("Preferred language for transcript."),
display_name=_("Preferred language for transcript"),
scope=Scope.preferences,
default="en"
)
transcript_download_format = String(
help="Transcript file format to download by user.",
help=_("Transcript file format to download by user."),
scope=Scope.preferences,
values=[
# Translators: This is a type of file used for captioning in the video player.
@@ -131,22 +132,22 @@ class VideoFields(object):
default='srt',
)
speed = Float(
help="The last speed that the user specified for the video.",
help=_("The last speed that the user specified for the video."),
scope=Scope.user_state,
)
global_speed = Float(
help="The default speed for the video.",
help=_("The default speed for the video."),
scope=Scope.preferences,
default=1.0
)
youtube_is_available = Boolean(
help="Specify whether YouTube is available for the user.",
help=_("Specify whether YouTube is available for the user."),
scope=Scope.user_info,
default=True
)
handout = String(
help="Upload a handout to accompany this video. Students can download the handout by clicking Download Handout under the video.",
display_name="Upload Handout",
help=_("Upload a handout to accompany this video. Students can download the handout by clicking Download Handout under the video."),
display_name=_("Upload Handout"),
scope=Scope.settings,
)

View File

@@ -7,6 +7,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String
from xmodule.annotator_token import retrieve_token
import textwrap
@@ -31,7 +32,7 @@ class AnnotatableFields(object):
sourceurl = String(help="The external source URL for the video.", display_name="Source URL", scope=Scope.settings, default="http://video-js.zencoder.com/oceans-clip.mp4")
poster_url = String(help="Poster Image URL", display_name="Poster URL", scope=Scope.settings, default="")
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation")
class VideoAnnotationModule(AnnotatableFields, XModule):
'''Video Annotation Module'''
@@ -55,73 +56,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode')
self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
def _get_annotation_class_attr(self, element):
""" Returns a dict with the CSS class attribute to set on the annotation
and an XML key to delete from the element.
"""
attr = {}
cls = ['annotatable-span', 'highlight']
highlight_key = 'highlight'
color = element.get(highlight_key)
if color is not None:
if color in self.highlight_colors:
cls.append('highlight-' + color)
attr['_delete'] = highlight_key
attr['value'] = ' '.join(cls)
return {'class': attr}
def _get_annotation_data_attr(self, element):
""" Returns a dict in which the keys are the HTML data attributes
to set on the annotation element. Each data attribute has a
corresponding 'value' and (optional) '_delete' key to specify
an XML attribute to delete.
"""
data_attrs = {}
attrs_map = {
'body': 'data-comment-body',
'title': 'data-comment-title',
'problem': 'data-problem-id'
}
for xml_key in attrs_map.keys():
if xml_key in element.attrib:
value = element.get(xml_key, '')
html_key = attrs_map[xml_key]
data_attrs[html_key] = {'value': value, '_delete': xml_key}
return data_attrs
def _render_annotation(self, element):
""" Renders an annotation element for HTML output. """
attr = {}
attr.update(self._get_annotation_class_attr(element))
attr.update(self._get_annotation_data_attr(element))
element.tag = 'span'
for key in attr.keys():
element.set(key, attr[key]['value'])
if '_delete' in attr[key] and attr[key]['_delete'] is not None:
delete_key = attr[key]['_delete']
del element.attrib[delete_key]
def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content)
xmltree.tag = 'div'
if 'display_name' in xmltree.attrib:
del xmltree.attrib['display_name']
for element in xmltree.findall('.//annotation'):
self._render_annotation(element)
return etree.tostring(xmltree, encoding='unicode')
self.user_email = ""
if self.runtime.get_real_user is not None:
self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email
def _extract_instructions(self, xmltree):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
@@ -155,9 +92,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
'sourceUrl': self.sourceurl,
'typeSource': extension,
'poster': self.poster_url,
'alert': self,
'content_html': self._render_content(),
'annotation_storage': self.annotation_storage_url
'content_html': self.content,
'annotation_storage': self.annotation_storage_url,
'token': retrieve_token(self.user_email, self.annotation_token_secret),
}
return self.system.render_template('videoannotation.html', context)
@@ -172,6 +109,7 @@ class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor):
def non_editable_metadata_fields(self):
non_editable_fields = super(VideoAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
VideoAnnotationDescriptor.annotation_storage_url
VideoAnnotationDescriptor.annotation_storage_url,
VideoAnnotationDescriptor.annotation_token_secret,
])
return non_editable_fields

View File

@@ -763,6 +763,13 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
json_choice = field.to_json(json_choice)
return json_choice
def get_text(value):
"""Localize a text value that might be None."""
if value is None:
return None
else:
return self.runtime.service(self, "i18n").ugettext(value)
metadata_fields = {}
# Only use the fields from this class, not mixins
@@ -776,8 +783,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
# gets the 'default_value' and 'explicitly_set' attrs
metadata_fields[field.name] = self.runtime.get_field_provenance(self, field)
metadata_fields[field.name]['field_name'] = field.name
metadata_fields[field.name]['display_name'] = field.display_name
metadata_fields[field.name]['help'] = field.help
metadata_fields[field.name]['display_name'] = get_text(field.display_name)
metadata_fields[field.name]['help'] = get_text(field.help)
metadata_fields[field.name]['value'] = field.read_json(self)
# We support the following editors: