Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
-c constraints.txt

django-statici18n
edx-django-utils
edx-i18n-tools
edx-opaque-keys
nh3
oauthlib
openedx-django-pyfs
wrapt
XBlock
3 changes: 3 additions & 0 deletions xblocks_contrib/video/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/*


48 changes: 48 additions & 0 deletions xblocks_contrib/video/ajax_handler_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
""" Mixin that provides AJAX handling for Video XBlock """
from webob import Response
from webob.multidict import MultiDict
from xblock.core import XBlock


class AjaxHandlerMixin:
"""
Mixin that provides AJAX handling for Video XBlock
"""
@property
def ajax_url(self):
"""
Returns the URL for the ajax handler.
"""
return self.runtime.handler_url(self, 'ajax_handler', '', '').rstrip('/?')

@XBlock.handler
def ajax_handler(self, request, suffix=None):
"""
XBlock handler that wraps `ajax_handler`
"""
class FileObjForWebobFiles:
"""
Turn Webob cgi.FieldStorage uploaded files into pure file objects.

Webob represents uploaded files as cgi.FieldStorage objects, which
have a .file attribute. We wrap the FieldStorage object, delegating
attribute access to the .file attribute. But the files have no
name, so we carry the FieldStorage .filename attribute as the .name.

"""
def __init__(self, webob_file):
self.file = webob_file.file
self.name = webob_file.filename

def __getattr__(self, name):
return getattr(self.file, name)

# WebOb requests have multiple entries for uploaded files. handle_ajax
# expects a single entry as a list.
request_post = MultiDict(request.POST)
for key in set(request.POST.keys()):
if hasattr(request.POST[key], "file"):
request_post[key] = list(map(FileObjForWebobFiles, request.POST.getall(key)))

response_data = self.handle_ajax(suffix, request_post)
return Response(response_data, content_type='application/json', charset='UTF-8')
147 changes: 147 additions & 0 deletions xblocks_contrib/video/bumper_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
Utils for video bumper
"""


import copy
import json
import logging
from collections import OrderedDict
from datetime import datetime, timedelta

import pytz
from django.conf import settings

from .video_utils import set_query_parameter

try:
import edxval.api as edxval_api
except ImportError:
edxval_api = None

log = logging.getLogger(__name__)


def get_bumper_settings(video):
"""
Get bumper settings from video instance.
"""
bumper_settings = copy.deepcopy(getattr(video, 'video_bumper', {}))

# clean up /static/ prefix from bumper transcripts
for lang, transcript_url in bumper_settings.get('transcripts', {}).items():
bumper_settings['transcripts'][lang] = transcript_url.replace("/static/", "")

return bumper_settings


def is_bumper_enabled(video):
"""
Check if bumper enabled.

- Feature flag ENABLE_VIDEO_BUMPER should be set to True
- Do not show again button should not be clicked by user.
- Current time minus periodicity must be greater that last time viewed
- edxval_api should be presented

Returns:
bool.
"""
bumper_last_view_date = getattr(video, 'bumper_last_view_date', None)
utc_now = datetime.utcnow().replace(tzinfo=pytz.utc)
periodicity = settings.FEATURES.get('SHOW_BUMPER_PERIODICITY', 0)
has_viewed = any([
video.bumper_do_not_show_again,
(bumper_last_view_date and bumper_last_view_date + timedelta(seconds=periodicity) > utc_now)
])
is_studio = getattr(video.runtime, "is_author_mode", False)
return bool(
not is_studio and
settings.FEATURES.get('ENABLE_VIDEO_BUMPER') and
get_bumper_settings(video) and
edxval_api and
not has_viewed
)


def bumperize(video):
"""
Populate video with bumper settings, if they are presented.
"""
video.bumper = {
'enabled': False,
'edx_video_id': "",
'transcripts': {},
'metadata': None,
}

if not is_bumper_enabled(video):
return

bumper_settings = get_bumper_settings(video)

try:
video.bumper['edx_video_id'] = bumper_settings['video_id']
video.bumper['transcripts'] = bumper_settings['transcripts']
except (TypeError, KeyError):
log.warning(
"Could not retrieve video bumper information from course settings"
)
return

sources = get_bumper_sources(video)
if not sources:
return

video.bumper.update({
'metadata': bumper_metadata(video, sources),
'enabled': True, # Video poster needs this.
})


def get_bumper_sources(video):
"""
Get bumper sources from edxval.

Returns list of sources.
"""
try:
val_profiles = ["desktop_webm", "desktop_mp4"]
val_video_urls = edxval_api.get_urls_for_profiles(video.bumper['edx_video_id'], val_profiles)
bumper_sources = [url for url in [val_video_urls[p] for p in val_profiles] if url]
except edxval_api.ValInternalError:
# if no bumper sources, nothing will be showed
log.warning(
"Could not retrieve information from VAL for Bumper edx Video ID: %s.", video.bumper['edx_video_id']
)
return []

return bumper_sources


def bumper_metadata(video, sources):
"""
Generate bumper metadata.
"""
transcripts = video.get_transcripts_info(is_bumper=True)
unused_track_url, bumper_transcript_language, bumper_languages = video.get_transcripts_for_student(transcripts)

metadata = OrderedDict({
'saveStateUrl': video.ajax_url + '/save_user_state',
'showCaptions': json.dumps(video.show_captions),
'sources': sources,
'streams': '',
'transcriptLanguage': bumper_transcript_language,
'transcriptLanguages': bumper_languages,
'transcriptTranslationUrl': set_query_parameter(
video.runtime.handler_url(video, 'transcript', 'translation/__lang__').rstrip('/?'), 'is_bumper', 1
),
'transcriptAvailableTranslationsUrl': set_query_parameter(
video.runtime.handler_url(video, 'transcript', 'available_translations').rstrip('/?'), 'is_bumper', 1
),
'publishCompletionUrl': set_query_parameter(
video.runtime.handler_url(video, 'publish_completion', '').rstrip('?'), 'is_bumper', 1
),
})

return metadata
95 changes: 95 additions & 0 deletions xblocks_contrib/video/cache_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
""" Cache Utils for Video Block """
import itertools

import wrapt
from django.utils.encoding import force_str
from edx_django_utils.cache import RequestCache


def request_cached(namespace=None, arg_map_function=None, request_cache_getter=None):
"""
A function decorator that automatically handles caching its return value for
the duration of the request. It returns the cached value for subsequent
calls to the same function, with the same parameters, within a given request.

Notes:
- We convert arguments and keyword arguments to their string form to build the cache key. So if you have
args/kwargs that can't be converted to strings, you're gonna have a bad time (don't do it).
- Cache key cardinality depends on the args/kwargs. So if you're caching a function that takes five arguments,
you might have deceptively low cache efficiency. Prefer functions with fewer arguments.
- WATCH OUT: Don't use this decorator for instance methods that take in a "self" argument that changes each
time the method is called. This will result in constant cache misses and not provide the performance benefit
you are looking for. Rather, change your instance method to a class method.
- Benchmark, benchmark, benchmark! If you never measure, how will you know you've improved? or regressed?

Arguments:
namespace (string): An optional namespace to use for the cache. By default, we use the default request cache,
not a namespaced request cache. Since the code automatically creates a unique cache key with the module and
function's name, storing the cached value in the default cache, you won't usually need to specify a
namespace value.
But you can specify a namespace value here if you need to use your own namespaced cache - for example,
if you want to clear out your own cache by calling RequestCache(namespace=NAMESPACE).clear().
NOTE: This argument is ignored if you supply a ``request_cache_getter``.
arg_map_function (function: arg->string): Function to use for mapping the wrapped function's arguments to
strings to use in the cache key. If not provided, defaults to force_text, which converts the given
argument to a string.
request_cache_getter (function: args, kwargs->RequestCache): Function that returns the RequestCache to use.
If not provided, defaults to edx_django_utils.cache.RequestCache. If ``request_cache_getter`` returns None,
the function's return values are not cached.

Returns:
func: a wrapper function which will call the wrapped function, passing in the same args/kwargs,
cache the value it returns, and return that cached value for subsequent calls with the
same args/kwargs within a single request.
"""
@wrapt.decorator
def decorator(wrapped, instance, args, kwargs):
"""
Arguments:
args, kwargs: values passed into the wrapped function
"""
# Check to see if we have a result in cache. If not, invoke our wrapped
# function. Cache and return the result to the caller.
if request_cache_getter:
request_cache = request_cache_getter(args if instance is None else (instance,) + args, kwargs)
else:
request_cache = RequestCache(namespace)

if request_cache:
cache_key = _func_call_cache_key(wrapped, arg_map_function, *args, **kwargs)
cached_response = request_cache.get_cached_response(cache_key)
if cached_response.is_found:
return cached_response.value

result = wrapped(*args, **kwargs)

if request_cache:
request_cache.set(cache_key, result)

return result

return decorator


def _func_call_cache_key(func, arg_map_function, *args, **kwargs):
"""
Returns a cache key based on the function's module,
the function's name, a stringified list of arguments
and a stringified list of keyword arguments.
"""
arg_map_function = arg_map_function or force_str

converted_args = list(map(arg_map_function, args))
converted_kwargs = list(map(arg_map_function, _sorted_kwargs_list(kwargs)))

cache_keys = [func.__module__, func.__name__] + converted_args + converted_kwargs
return '.'.join(cache_keys)


def _sorted_kwargs_list(kwargs):
"""
Returns a unique and deterministic ordered list from the given kwargs.
"""
sorted_kwargs = sorted(kwargs.items())
sorted_kwargs_list = list(itertools.chain(*sorted_kwargs))
return sorted_kwargs_list
17 changes: 17 additions & 0 deletions xblocks_contrib/video/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Constants used by DjangoXBlockUserService
"""

# This is the view that will be rendered to display the XBlock in the LMS.
# It will also be used to render the block in "preview" mode in Studio, unless
# the XBlock also implements author_view.
STUDENT_VIEW = 'student_view'

# This is the view that will be rendered to display the XBlock in the LMS for unenrolled learners.
# Implementations of this view should assume that a user and user data are not available.
PUBLIC_VIEW = 'public_view'

# The personally identifiable user ID.
ATTR_KEY_USER_ID = 'edx-platform.user_id'
# The country code determined from the user's request IP address.
ATTR_KEY_REQUEST_COUNTRY_CODE = 'edx-platform.request_country_code'
61 changes: 61 additions & 0 deletions xblocks_contrib/video/content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
""" Video Block Static Content, class copied from StaticContent class in edx-platform/xmodule/contentstore/content.py """
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import AssetKey
from opaque_keys.edx.locator import AssetLocator

class VideoBlockStaticContent: # lint-amnesty, pylint: disable=missing-class-docstring
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None,
length=None, locked=False, content_digest=None):
self.location = loc
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed # lint-amnesty, pylint: disable=line-too-long
self.content_type = content_type
self._data = data
self.length = length
self.last_modified_at = last_modified_at
self.thumbnail_location = thumbnail_location
# optional information about where this file was imported from. This is needed to support import/export
# cycles
self.import_path = import_path
self.locked = locked
self.content_digest = content_digest

@staticmethod
def compute_location(course_key, path, revision=None, is_thumbnail=False): # lint-amnesty, pylint: disable=unused-argument
"""
Constructs a location object for static content.

- course_key: the course that this asset belongs to
- path: is the name of the static asset
- revision: is the object's revision information
- is_thumbnail: is whether or not we want the thumbnail version of this
asset
"""
path = path.replace('/', '_')
return course_key.make_asset_key(
'asset' if not is_thumbnail else 'thumbnail',
AssetLocator.clean_keeping_underscores(path)
).for_branch(None)

@staticmethod
def get_location_from_path(path):
"""
Generate an AssetKey for the given path (old c4x/org/course/asset/name syntax)
"""
try:
return AssetKey.from_string(path)
except InvalidKeyError:
# TODO - re-address this once LMS-11198 is tackled.
if path.startswith('/') or path.endswith('/'):
# try stripping off the leading slash and try again
return AssetKey.from_string(path.strip('/'))

@staticmethod
def serialize_asset_key_with_slash(asset_key):
"""
Legacy code expects the serialized asset key to start w/ a slash; so, do that in one place
:param asset_key:
"""
url = str(asset_key)
if not url.startswith('/'):
url = '/' + url # TODO - re-address this once LMS-11198 is tackled.
return url
Loading
Loading