Merge branch 'release'

Conflicts:
	common/lib/xmodule/xmodule/video_module/video_module.py
	requirements/edx/github.txt
This commit is contained in:
Andy Armstrong
2014-09-23 15:53:10 -04:00
38 changed files with 1162 additions and 99 deletions

View File

@@ -37,6 +37,8 @@ from eventtracking import tracker
from importlib import import_module
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
import lms.lib.comment_client as cc
from util.query import use_read_replica_if_available
@@ -1018,6 +1020,14 @@ class CourseEnrollment(models.Model):
else:
return True
@property
def username(self):
return self.user.username
@property
def course(self):
return modulestore().get_course(self.course_id)
class CourseEnrollmentAllowed(models.Model):
"""
@@ -1064,7 +1074,7 @@ class CourseAccessRole(models.Model):
convenience function to make eq overrides easier and clearer. arbitrary decision
that role is primary, followed by org, course, and then user
"""
return (self.role, self.org, self.course_id, self.user)
return (self.role, self.org, self.course_id, self.user_id)
def __eq__(self, other):
"""

View File

@@ -18,7 +18,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
# This list is the minimal set required by the notification service
fields = ("id", "email", "name", "username", "preferences")
fields = ("id", "url", "email", "name", "username", "preferences")
read_only_fields = ("id", "email", "username")

View File

@@ -274,6 +274,13 @@ class CourseFields(object):
help=_("Enter true or false. If true, the course appears in the list of new courses on edx.org, and a New! badge temporarily appears next to the course image."),
scope=Scope.settings
)
mobile_available = Boolean(
display_name=_("Mobile Course Available"),
help=_("Enter true or false. If true, the course will be available to mobile devices."),
default=False,
scope=Scope.settings
)
no_grade = Boolean(
display_name=_("Course Not Graded"),
help=_("Enter true or false. If true, the course will not be graded."),

View File

@@ -506,3 +506,89 @@ class Transcript(object):
pass
return StaticContent.compute_location(location.course_key, filename)
class VideoTranscriptsMixin(object):
"""Mixin class for transcript functionality.
This is necessary for both VideoModule and VideoDescriptor.
"""
def available_translations(self, verify_assets=True):
"""Return a list of language codes for which we have transcripts.
Args:
verify_assets (boolean): If True, checks to ensure that the transcripts
really exist in the contentstore. If False, we just look at the
VideoDescriptor fields and do not query the contentstore. One reason
we might do this is to avoid slamming contentstore() with queries
when trying to make a listing of videos and their languages.
Defaults to True.
"""
translations = []
# If we're not verifying the assets, we just trust our field values
if not verify_assets:
if self.sub:
translations = ['en']
translations += list(self.transcripts)
return translations
# If we've gotten this far, we're going to verify that the transcripts
# being referenced are actually in the contentstore.
if self.sub: # check if sjson exists for 'en'.
try:
Transcript.asset(self.location, self.sub, 'en')
except NotFoundError:
pass
else:
translations = ['en']
for lang in self.transcripts:
try:
Transcript.asset(self.location, None, None, self.transcripts[lang])
except NotFoundError:
continue
translations.append(lang)
return translations
def get_transcript(self, transcript_format='srt', lang=None):
"""
Returns transcript, filename and MIME type.
Raises:
- NotFoundError if cannot find transcript file in storage.
- ValueError if transcript file is empty or incorrect JSON.
- KeyError if transcript file has incorrect format.
If language is 'en', self.sub should be correct subtitles name.
If language is 'en', but if self.sub is not defined, this means that we
should search for video name in order to get proper transcript (old style courses).
If language is not 'en', give back transcript in proper language and format.
"""
if not lang:
lang = self.transcript_language
if lang == 'en':
if self.sub: # HTML5 case and (Youtube case for new style videos)
transcript_name = self.sub
elif self.youtube_id_1_0: # old courses
transcript_name = self.youtube_id_1_0
else:
log.debug("No subtitles for 'en' language")
raise ValueError
data = Transcript.asset(self.location, transcript_name, lang).data
filename = u'{}.{}'.format(transcript_name, transcript_format)
content = Transcript.convert(data, 'sjson', transcript_format)
else:
data = Transcript.asset(self.location, None, None, self.transcripts[lang]).data
filename = u'{}.{}'.format(os.path.splitext(self.transcripts[lang])[0], transcript_format)
content = Transcript.convert(data, 'srt', transcript_format)
if not content:
log.debug('no subtitles produced in get_transcript')
raise ValueError
return content, filename, Transcript.mime_types[transcript_format]

View File

@@ -133,45 +133,6 @@ class VideoStudentViewHandlers(object):
else:
return get_or_create_sjson(self)
def get_transcript(self, transcript_format='srt'):
"""
Returns transcript, filename and MIME type.
Raises:
- NotFoundError if cannot find transcript file in storage.
- ValueError if transcript file is empty or incorrect JSON.
- KeyError if transcript file has incorrect format.
If language is 'en', self.sub should be correct subtitles name.
If language is 'en', but if self.sub is not defined, this means that we
should search for video name in order to get proper transcript (old style courses).
If language is not 'en', give back transcript in proper language and format.
"""
lang = self.transcript_language
if lang == 'en':
if self.sub: # HTML5 case and (Youtube case for new style videos)
transcript_name = self.sub
elif self.youtube_id_1_0: # old courses
transcript_name = self.youtube_id_1_0
else:
log.debug("No subtitles for 'en' language")
raise ValueError
data = Transcript.asset(self.location, transcript_name, lang).data
filename = u'{}.{}'.format(transcript_name, transcript_format)
content = Transcript.convert(data, 'sjson', transcript_format)
else:
data = Transcript.asset(self.location, None, None, self.transcripts[lang]).data
filename = u'{}.{}'.format(os.path.splitext(self.transcripts[lang])[0], transcript_format)
content = Transcript.convert(data, 'srt', transcript_format)
if not content:
log.debug('no subtitles produced in get_transcript')
raise ValueError
return content, filename, Transcript.mime_types[transcript_format]
def get_static_transcript(self, request):
"""
Courses that are imported with the --nostatic flag do not show
@@ -283,20 +244,7 @@ class VideoStudentViewHandlers(object):
response.content_type = transcript_mime_type
elif dispatch == 'available_translations':
available_translations = []
if self.sub: # check if sjson exists for 'en'.
try:
Transcript.asset(self.location, self.sub, 'en')
except NotFoundError:
pass
else:
available_translations = ['en']
for lang in self.transcripts:
try:
Transcript.asset(self.location, None, None, self.transcripts[lang])
except NotFoundError:
continue
available_translations.append(lang)
available_translations = self.available_translations()
if available_translations:
response = Response(json.dumps(available_translations))
response.content_type = 'application/json'

View File

@@ -15,38 +15,71 @@ Examples of html5 videos for manual testing:
https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm
https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv
"""
import copy
import json
import logging
import os.path
from collections import OrderedDict
from operator import itemgetter
from lxml import etree
from pkg_resources import resource_string
import copy
from collections import OrderedDict
from django.conf import settings
from xblock.fields import ScopeIds
from xblock.runtime import KvsFieldData
from xmodule.exceptions import NotFoundError
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, own_metadata
from xmodule.x_module import XModule, module_attr
from xmodule.editing_module import TabsEditingDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field
from .transcripts_utils import Transcript, VideoTranscriptsMixin
from .video_utils import create_youtube_string, get_video_from_cdn
from .video_xfields import VideoFields
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
from xmodule.video_module import manage_video_subtitles_save
# The following import/except block for edxval is temporary measure until
# edxval is a proper XBlock Runtime Service.
#
# Here's the deal: the VideoModule should be able to take advantage of edx-val
# (https://github.com/edx/edx-val) to figure out what URL to give for video
# resources that have an edx_video_id specified. edx-val is a Django app, and
# including it causes tests to fail because we run common/lib tests standalone
# without Django dependencies. The alternatives seem to be:
#
# 1. Move VideoModule out of edx-platform.
# 2. Accept the Django dependency in common/lib.
# 3. Try to import, catch the exception on failure, and check for the existence
# of edxval_api before invoking it in the code.
# 4. Make edxval an XBlock Runtime Service
#
# (1) is a longer term goal. VideoModule should be made into an XBlock and
# extracted from edx-platform entirely. But that's expensive to do because of
# the various dependencies (like templates). Need to sort this out.
# (2) is explicitly discouraged.
# (3) is what we're doing today. The code is still functional when called within
# the context of the LMS, but does not cause failure on import when running
# standalone tests. Most VideoModule tests tend to be in the LMS anyway,
# probably for historical reasons, so we're not making things notably worse.
# (4) is one of the next items on the backlog for edxval, and should get rid
# of this particular import silliness. It's just that I haven't made one before,
# and I was worried about trying it with my deadline constraints.
try:
import edxval.api as edxval_api
except ImportError:
edxval_api = None
log = logging.getLogger(__name__)
_ = lambda text: text
class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, XModule):
"""
XML source example:
@@ -96,34 +129,22 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
]}
js_module_name = "Video"
def get_html(self):
def get_transcripts_for_student(self):
"""Return transcript information necessary for rendering the XModule student view.
This is more or less a direct extraction from `get_html`.
Returns:
Tuple of (track_url, transcript_language, sorted_languages)
track_url -> subtitle download url
transcript_language -> default transcript language
sorted_languages -> dictionary of available transcript languages
"""
track_url = None
download_video_link = None
transcript_download_format = self.transcript_download_format
sources = filter(None, self.html5_sources)
# If the user comes from China use China CDN for html5 videos.
# 'CN' is China ISO 3166-1 country code.
# Video caching is disabled for Studio. User_location is always None in Studio.
# CountryMiddleware disabled for Studio.
cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(self.system.user_location)
if getattr(self, 'video_speed_optimizations', True) and cdn_url:
for index, source_url in enumerate(sources):
new_url = get_video_from_cdn(cdn_url, source_url)
if new_url:
sources[index] = new_url
if self.download_video:
if self.source:
download_video_link = self.source
elif self.html5_sources:
download_video_link = self.html5_sources[0]
if self.download_track:
if self.track:
track_url = self.track
transcript_download_format = None
elif self.sub or self.transcripts:
track_url = self.runtime.handler_url(self, 'transcript', 'download').rstrip('/?')
@@ -154,6 +175,52 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
sorted_languages.insert(0, ('table', 'Table of Contents'))
sorted_languages = OrderedDict(sorted_languages)
return track_url, transcript_language, sorted_languages
def get_html(self):
transcript_download_format = self.transcript_download_format if not (self.download_track and self.track) else None
sources = filter(None, self.html5_sources)
# If the user comes from China use China CDN for html5 videos.
# 'CN' is China ISO 3166-1 country code.
# Video caching is disabled for Studio. User_location is always None in Studio.
# CountryMiddleware disabled for Studio.
cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(self.system.user_location)
if getattr(self, 'video_speed_optimizations', True) and cdn_url:
for index, source_url in enumerate(sources):
new_url = get_video_from_cdn(cdn_url, source_url)
if new_url:
sources[index] = new_url
download_video_link = None
youtube_streams = ""
# If we have an edx_video_id, we prefer its values over what we store
# internally for download links (source, html5_sources) and the youtube
# stream.
if self.edx_video_id and edxval_api:
val_video_urls = edxval_api.get_urls_for_profiles(
self.edx_video_id, ["desktop_mp4", "youtube"]
)
# VAL will always give us the keys for the profiles we asked for, but
# if it doesn't have an encoded video entry for that Video + Profile, the
# value will map to `None`
if val_video_urls["desktop_mp4"]:
download_video_link = val_video_urls["desktop_mp4"]
if val_video_urls["youtube"]:
youtube_streams = "1.00:{}".format(val_video_urls["youtube"])
# If there was no edx_video_id, or if there was no download specified
# for it, we fall back on whatever we find in the VideoDescriptor
if not download_video_link and self.download_video:
if self.source:
download_video_link = self.source
elif self.html5_sources:
download_video_link = self.html5_sources[0]
track_url, transcript_language, sorted_languages = self.get_transcripts_for_student()
return self.system.render_template('video.html', {
'ajax_url': self.system.ajax_url + '/save_user_state',
@@ -174,7 +241,7 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
'start': self.start_time.total_seconds(),
'sub': self.sub,
'track': track_url,
'youtube_streams': create_youtube_string(self),
'youtube_streams': youtube_streams or create_youtube_string(self),
# TODO: Later on the value 1500 should be taken from some global
# configuration setting field.
'yt_test_timeout': 1500,
@@ -189,7 +256,7 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
})
class VideoDescriptor(VideoFields, VideoStudioViewHandlers, TabsEditingDescriptor, EmptyDataRawDescriptor):
class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, TabsEditingDescriptor, EmptyDataRawDescriptor):
"""
Descriptor for `VideoModule`.
"""
@@ -403,6 +470,13 @@ class VideoDescriptor(VideoFields, VideoStudioViewHandlers, TabsEditingDescripto
youtube_id_1_0 = metadata_fields['youtube_id_1_0']
def get_youtube_link(video_id):
# First try a lookup in VAL. If we have a YouTube entry there, it overrides the
# one passed in.
if self.edx_video_id and edxval_api:
val_youtube_id = edxval_api.get_url_for_profile(self.edx_video_id, "youtube")
if val_youtube_id:
video_id = val_youtube_id
if video_id:
return 'http://youtu.be/{0}'.format(video_id)
else:

View File

@@ -1,5 +1,5 @@
"""
Module containts utils specific for video_module but not for transcripts.
Module contains utils specific for video_module but not for transcripts.
"""
import json
import logging

View File

@@ -145,9 +145,14 @@ class VideoFields(object):
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"),
scope=Scope.settings,
)
edx_video_id = String(
help=_('Optional. Use this for videos where download and streaming URLs for the videos are completely managed by edX. This will override the settings for "Default Video URL", "Video File URLs", and all YouTube IDs. If you do not know what this setting is, you can leave it blank and continue to use these other settings.'),
display_name=_("EdX Video ID"),
scope=Scope.settings,
default="",
)

View File

@@ -60,6 +60,7 @@ DEFAULT_SETTINGS = [
['Default Timed Transcript', '', False],
['Download Transcript Allowed', 'False', False],
['Downloadable Transcript URL', '', False],
['EdX Video ID', '', False],
['Show Transcript', 'True', False],
['Transcript Languages', '', False],
['Upload Handout', '', False],