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

@@ -232,10 +232,9 @@ def get_course_about_section(course, section_key):
raise KeyError("Invalid about key " + str(section_key))
def get_course_info_section(request, course, section_key):
def get_course_info_section_module(request, course, section_key):
"""
This returns the snippet of html to be rendered on the course info page,
given the key for the section.
This returns the course info module for a given section_key.
Valid keys:
- handouts
@@ -247,7 +246,8 @@ def get_course_info_section(request, course, section_key):
# Use an empty cache
field_data_cache = FieldDataCache([], course.id, request.user)
info_module = get_module(
return get_module(
request.user,
request,
usage_key,
@@ -255,10 +255,22 @@ def get_course_info_section(request, course, section_key):
log_if_not_found=False,
wrap_xmodule_display=False,
static_asset_path=course.static_asset_path
)
)
def get_course_info_section(request, course, section_key):
"""
This returns the snippet of html to be rendered on the course info page,
given the key for the section.
Valid keys:
- handouts
- guest_handouts
- updates
- guest_updates
"""
info_module = get_course_info_section_module(request, course, section_key)
html = ''
if info_module is not None:
try:
html = info_module.render(STUDENT_VIEW).content

View File

@@ -15,6 +15,13 @@ from xmodule.x_module import STUDENT_VIEW
from xmodule.tests import get_test_descriptor_system
from xmodule.tests.test_video import VideoDescriptorTestBase
from edxval.api import (
ValVideoNotFoundError,
get_video_info,
create_profile,
create_video
)
import mock
from . import BaseTestXmodule
from .test_video_xml import SOURCE_XML
@@ -365,6 +372,250 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
)
def test_get_html_with_non_existant_edx_video_id(self):
"""
Tests the VideoModule get_html where a edx_video_id is given but a video is not found
"""
SOURCE_XML = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
start_time="01:00:03" end_time="01:00:10"
edx_video_id="{edx_video_id}"
>
{sources}
</video>
"""
no_video_data = {
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'edx_video_id':"meow",
'result': {
'download_video_link': u'example_source.mp4',
'sources': json.dumps([u'example.mp4', u'example.webm']),
}
}
DATA = SOURCE_XML.format(
download_video=no_video_data['download_video'],
source=no_video_data['source'],
sources=no_video_data['sources'],
edx_video_id=no_video_data['edx_video_id']
)
self.initialize_module(data=DATA)
# Referencing a non-existent VAL ID in courseware won't cause an error --
# it'll just fall back to the values in the VideoDescriptor.
self.assertIn("example_source.mp4", self.item_descriptor.render(STUDENT_VIEW).content)
@mock.patch('edxval.api.get_video_info')
def test_get_html_with_mocked_edx_video_id(self, mock_get_video_info):
mock_get_video_info.return_value = {
'url' : '/edxval/video/example',
'edx_video_id': u'example',
'duration': 111.0,
'client_video_id': u'The example video',
'encoded_videos': [
{
'url': u'http://www.meowmix.com',
'file_size': 25556,
'bitrate': 9600,
'profile': u'desktop_mp4'
}
]
}
SOURCE_XML = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
start_time="01:00:03" end_time="01:00:10"
edx_video_id="{edx_video_id}"
>
{sources}
</video>
"""
data = {
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'edx_video_id': "mock item",
'result': {
'download_video_link': u'http://www.meowmix.com',
'sources': json.dumps([u'example.mp4', u'example.webm']),
}
}
# Video found for edx_video_id
initial_context = {
'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true',
'handout': None,
'display_name': u'A Name',
'download_video_link': None,
'end': 3610.0,
'id': None,
'sources': '[]',
'speed': 'null',
'general_speed': 1.0,
'start': 3603.0,
'saved_video_position': 0.0,
'sub': u'a_sub_file.srt.sjson',
'track': None,
'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500,
'yt_api_url': 'www.youtube.com/iframe_api',
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
'transcript_language': u'en',
'transcript_languages': '{"en": "English"}',
}
DATA = SOURCE_XML.format(
download_video=data['download_video'],
source=data['source'],
sources=data['sources'],
edx_video_id=data['edx_video_id']
)
self.initialize_module(data=DATA)
context = self.item_descriptor.render(STUDENT_VIEW).content
expected_context = dict(initial_context)
expected_context.update({
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'translation'
).rstrip('/?'),
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'),
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'id': self.item_descriptor.location.html_id(),
})
expected_context.update(data['result'])
self.assertEqual(
context,
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
)
def test_get_html_with_existing_edx_video_id(self):
result = create_profile(
dict(
profile_name="desktop_mp4",
extension="mp4",
width=200,
height=2001
)
)
self.assertEqual(result, "desktop_mp4")
result = create_video(
dict(
client_video_id="Thunder Cats",
duration=111,
edx_video_id="thundercats",
encoded_videos=[
dict(
url="http://fake-video.edx.org/thundercats.mp4",
file_size=9000,
bitrate=42,
profile="desktop_mp4",
)
]
)
)
self.assertEqual(result, "thundercats")
SOURCE_XML = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
start_time="01:00:03" end_time="01:00:10"
edx_video_id="{edx_video_id}"
>
{sources}
</video>
"""
data = {
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'edx_video_id':"thundercats",
'result': {
'download_video_link': u'http://fake-video.edx.org/thundercats.mp4',
'sources': json.dumps([u'example.mp4', u'example.webm']),
}
}
# Video found for edx_video_id
initial_context = {
'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true',
'handout': None,
'display_name': u'A Name',
'download_video_link': None,
'end': 3610.0,
'id': None,
'sources': '[]',
'speed': 'null',
'general_speed': 1.0,
'start': 3603.0,
'saved_video_position': 0.0,
'sub': u'a_sub_file.srt.sjson',
'track': None,
'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500,
'yt_api_url': 'www.youtube.com/iframe_api',
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
'transcript_language': u'en',
'transcript_languages': '{"en": "English"}',
}
DATA = SOURCE_XML.format(
download_video=data['download_video'],
source=data['source'],
sources=data['sources'],
edx_video_id=data['edx_video_id']
)
self.initialize_module(data=DATA)
context = self.item_descriptor.render(STUDENT_VIEW).content
expected_context = dict(initial_context)
expected_context.update({
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'translation'
).rstrip('/?'),
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'),
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'id': self.item_descriptor.location.html_id(),
})
expected_context.update(data['result'])
self.assertEqual(
context,
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
)
@patch('xmodule.video_module.video_module.get_video_from_cdn')
def test_get_html_cdn_source(self, mocked_get_video):
"""
@@ -465,6 +716,107 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
)
@patch('xmodule.video_module.video_module.get_video_from_cdn')
def test_get_html_cdn_source(self, mocked_get_video):
"""
Test if sources got from CDN.
"""
def side_effect(*args, **kwargs):
cdn = {
'http://example.com/example.mp4': 'http://cdn_example.com/example.mp4',
'http://example.com/example.webm': 'http://cdn_example.com/example.webm',
}
return cdn.get(args[1])
mocked_get_video.side_effect = side_effect
SOURCE_XML = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
start_time="01:00:03" end_time="01:00:10"
>
{sources}
</video>
"""
cases = [
{
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="http://example.com/example.mp4"/>
<source src="http://example.com/example.webm"/>
""",
'result': {
'download_video_link': u'example_source.mp4',
'sources': json.dumps(
[
u'http://cdn_example.com/example.mp4',
u'http://cdn_example.com/example.webm'
]
),
},
},
]
initial_context = {
'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true',
'handout': None,
'display_name': u'A Name',
'download_video_link': None,
'end': 3610.0,
'id': None,
'sources': '[]',
'speed': 'null',
'general_speed': 1.0,
'start': 3603.0,
'saved_video_position': 0.0,
'sub': u'a_sub_file.srt.sjson',
'track': None,
'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500,
'yt_api_url': 'www.youtube.com/iframe_api',
'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
'transcript_language': u'en',
'transcript_languages': '{"en": "English"}',
}
for data in cases:
DATA = SOURCE_XML.format(
download_video=data['download_video'],
source=data['source'],
sources=data['sources']
)
self.initialize_module(data=DATA)
self.item_descriptor.xmodule_runtime.user_location = 'CN'
context = self.item_descriptor.render('student_view').content
expected_context = dict(initial_context)
expected_context.update({
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'translation'
).rstrip('/?'),
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'),
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'id': self.item_descriptor.location.html_id(),
})
expected_context.update(data['result'])
self.assertEqual(
context,
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
)
class TestVideoDescriptorInitialization(BaseTestXmodule):
"""
Make sure that module initialization works correctly.

View File

View File

@@ -0,0 +1 @@
# A models.py is required to make this an app (until we move to Django 1.7)

View File

@@ -0,0 +1,26 @@
from django.conf.urls import patterns, url, include
from django.conf import settings
from rest_framework import routers
from rest_framework.urlpatterns import format_suffix_patterns
from .views import CourseAboutDetail, CourseUpdatesList, CourseHandoutsList
urlpatterns = patterns(
'mobile_api.course_info.views',
url(
r'^{}/about$'.format(settings.COURSE_ID_PATTERN),
CourseAboutDetail.as_view(),
name='course-about-detail'
),
url(
r'^{}/handouts$'.format(settings.COURSE_ID_PATTERN),
CourseHandoutsList.as_view(),
name='course-handouts-list'
),
url(
r'^{}/updates$'.format(settings.COURSE_ID_PATTERN),
CourseUpdatesList.as_view(),
name='course-updates-list'
),
)

View File

@@ -0,0 +1,59 @@
from rest_framework import generics, permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework.response import Response
from rest_framework.views import APIView
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module
from courseware.courses import get_course_about_section, get_course_info_section_module
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
from student.models import CourseEnrollment, User
class CourseUpdatesList(generics.ListAPIView):
"""Notes:
1. This only works for new-style course updates and is not the older freeform
format.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
def list(self, request, *args, **kwargs):
course_id = CourseKey.from_string(kwargs['course_id'])
course = modulestore().get_course(course_id)
course_updates_module = get_course_info_section_module(request, course, 'updates')
return Response(reversed(course_updates_module.items))
class CourseHandoutsList(generics.ListAPIView):
"""Please just render this in an HTML view for now.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
def list(self, request, *args, **kwargs):
course_id = CourseKey.from_string(kwargs['course_id'])
course = modulestore().get_course(course_id)
course_handouts_module = get_course_info_section_module(request, course, 'handouts')
return Response({'handouts_html': course_handouts_module.data})
class CourseAboutDetail(generics.RetrieveAPIView):
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, *args, **kwargs):
course_id = CourseKey.from_string(kwargs['course_id'])
course = modulestore().get_course(course_id)
# There are other fields, but they don't seem to be in use.
# see courses.py:get_course_about_section.
#
# This can also return None, so check for that before calling strip()
about_section_html = get_course_about_section(course, "overview")
return Response(
{"overview": about_section_html.strip() if about_section_html else ""}
)

View File

@@ -0,0 +1 @@
# A models.py is required to make this an app (until we move to Django 1.7)

View File

@@ -0,0 +1,12 @@
from django.conf.urls import patterns, url, include
from rest_framework import routers
from .users.views import my_user_info
# Additionally, we include login URLs for the browseable API.
urlpatterns = patterns('',
url(r'^users/', include('mobile_api.users.urls')),
url(r'^my_user_info', my_user_info),
url(r'^video_outlines/', include('mobile_api.video_outlines.urls')),
url(r'^course_info/', include('mobile_api.course_info.urls')),
)

View File

@@ -0,0 +1 @@
# A models.py is required to make this an app (until we move to Django 1.7)

View File

@@ -0,0 +1,80 @@
from rest_framework import serializers
from rest_framework.reverse import reverse
from xmodule.modulestore.search import path_to_location
from courseware.courses import course_image_url
from student.models import CourseEnrollment, User
class CourseField(serializers.RelatedField):
"""Custom field to wrap a CourseDescriptor object. Read-only."""
def to_native(self, course):
course_id = unicode(course.id)
request = self.context.get('request', None)
if request:
video_outline_url = reverse(
'video-summary-list',
kwargs={'course_id': course_id},
request=request
)
course_updates_url = reverse(
'course-updates-list',
kwargs={'course_id': course_id},
request=request
)
course_handouts_url = reverse(
'course-handouts-list',
kwargs={'course_id': course_id},
request=request
)
course_about_url = reverse(
'course-about-detail',
kwargs={'course_id': course_id},
request=request
)
else:
video_outline_url = None
course_updates_url = None
course_handouts_url = None
course_about_url = None
return {
"id": course_id,
"name": course.display_name,
"number": course.number,
"org": course.display_org_with_default,
"start": course.start,
"end": course.end,
"course_image": course_image_url(course),
"latest_updates": {
"video": None
},
"video_outline": video_outline_url,
"course_updates": course_updates_url,
"course_handouts": course_handouts_url,
"course_about": course_about_url,
}
class CourseEnrollmentSerializer(serializers.ModelSerializer):
course = CourseField()
class Meta:
model = CourseEnrollment
fields = ('created', 'mode', 'is_active', 'course')
lookup_field = 'username'
class UserSerializer(serializers.HyperlinkedModelSerializer):
name = serializers.Field(source='profile.name')
course_enrollments = serializers.HyperlinkedIdentityField(
view_name='courseenrollment-detail',
lookup_field='username'
)
class Meta:
model = User
fields = ('id', 'username', 'email', 'name', 'course_enrollments')
lookup_field = 'username'

View File

@@ -0,0 +1,16 @@
from django.conf.urls import patterns, url, include
from rest_framework import routers
from rest_framework.urlpatterns import format_suffix_patterns
from .views import UserDetail, UserCourseEnrollmentsList
urlpatterns = patterns('mobile_api.users.views',
url(r'^(?P<username>\w+)$', UserDetail.as_view(), name='user-detail'),
url(
r'^(?P<username>\w+)/course_enrollments/$',
UserCourseEnrollmentsList.as_view(),
name='courseenrollment-detail'
),
)

View File

@@ -0,0 +1,79 @@
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from rest_framework import generics, permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from courseware.access import has_access
from student.forms import PasswordResetFormNoActive
from student.models import CourseEnrollment, User
from xmodule.modulestore.django import modulestore
from .serializers import CourseEnrollmentSerializer, UserSerializer
class IsUser(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return request.user == obj
class UserDetail(generics.RetrieveAPIView):
"""Read-only information about our User.
This will be where users are redirected to after API login and will serve
as a place to list all useful resources this user can access.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated, IsUser)
queryset = (
User.objects.all()
.select_related('profile', 'course_enrollments')
)
serializer_class = UserSerializer
lookup_field = 'username'
class UserCourseEnrollmentsList(generics.ListAPIView):
"""Read-only list of courses that this user is enrolled in."""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated, IsUser)
queryset = CourseEnrollment.objects.all()
serializer_class = CourseEnrollmentSerializer
lookup_field = 'username'
def get_queryset(self):
qset = self.queryset.filter(
user__username=self.kwargs['username'], is_active=True
).order_by('created')
return mobile_course_enrollments(qset, self.request.user)
def get(self, request, *args, **kwargs):
if request.user.username != kwargs['username']:
raise PermissionDenied
return super(UserCourseEnrollmentsList, self).get(self, request, *args, **kwargs)
@api_view(["GET"])
@authentication_classes((OAuth2Authentication, SessionAuthentication))
@permission_classes((IsAuthenticated,))
def my_user_info(request):
if not request.user:
raise PermissionDenied
return redirect("user-detail", username=request.user.username)
def mobile_course_enrollments(enrollments, user):
"""
Return enrollments only if courses are mobile_available (or if the user has staff access)
enrollments is a list of CourseEnrollments.
"""
for enr in enrollments:
course = enr.course
# The course doesn't always really exist -- we can have bad data in the enrollments
# pointing to non-existent (or removed) courses, in which case `course` is None.
if course and (course.mobile_available or has_access(user, 'staff', course)):
yield enr

View File

@@ -0,0 +1 @@
# A models.py is required to make this an app (until we move to Django 1.7)

View File

@@ -0,0 +1,140 @@
from rest_framework.reverse import reverse
from courseware.access import has_access
from edxval.api import (
get_video_info_for_course_and_profile, ValInternalError
)
class BlockOutline(object):
def __init__(self, course_id, start_block, categories_to_outliner, request):
"""Create a BlockOutline using `start_block` as a starting point."""
self.start_block = start_block
self.categories_to_outliner = categories_to_outliner
self.course_id = course_id
self.request = request # needed for making full URLS
self.local_cache = {}
try:
self.local_cache['course_videos'] = get_video_info_for_course_and_profile(
unicode(course_id), "mobile_low"
)
except ValInternalError:
self.local_cache['course_videos'] = {}
def __iter__(self):
child_to_parent = {}
stack = [self.start_block]
# path should be optional
def path(block):
block_path = []
while block in child_to_parent:
block = child_to_parent[block]
if block is not self.start_block:
block_path.append({
'name': block.display_name,
'category': block.category,
})
return reversed(block_path)
def find_urls(block):
block_path = []
while block in child_to_parent:
block = child_to_parent[block]
block_path.append(block)
course, chapter, section, unit = list(reversed(block_path))[:4]
position = 1
unit_name = unit.url_name
for block in section.children:
if block.name == unit_name:
break
position += 1
kwargs = dict(
course_id=course.id.to_deprecated_string(),
chapter=chapter.url_name,
section=section.url_name
)
section_url = reverse(
"courseware_section",
kwargs=kwargs,
request=self.request,
)
kwargs['position'] = position
unit_url = reverse(
"courseware_position",
kwargs=kwargs,
request=self.request,
)
return unit_url, section_url
user = self.request.user
while stack:
curr_block = stack.pop()
if curr_block.category in self.categories_to_outliner:
if not has_access(user, 'load', curr_block, course_key=self.course_id):
continue
summary_fn = self.categories_to_outliner[curr_block.category]
block_path = list(path(block))
unit_url, section_url = find_urls(block)
yield {
"path": block_path,
"named_path": [b["name"] for b in block_path[:-1]],
"unit_url": unit_url,
"section_url": section_url,
"summary": summary_fn(self.course_id, curr_block, self.request, self.local_cache)
}
if curr_block.has_children:
for block in reversed(curr_block.get_children()):
stack.append(block)
child_to_parent[block] = curr_block
def video_summary(course, course_id, video_descriptor, request, local_cache):
# First try to check VAL for the URLs we want.
val_video_info = local_cache['course_videos'].get(video_descriptor.edx_video_id, {})
if val_video_info:
video_url = val_video_info['url']
# Then fall back to VideoDescriptor fields for video URLs
elif video_descriptor.html5_sources:
video_url = video_descriptor.html5_sources[0]
else:
video_url = video_descriptor.source
# If we have the video information from VAL, we also have duration and size.
duration = val_video_info.get('duration', None)
size = val_video_info.get('file_size', 0)
# Transcripts...
transcript_langs = video_descriptor.available_translations(verify_assets=False)
transcripts = {
lang: reverse(
'video-transcripts-detail',
kwargs={
'course_id': unicode(course_id),
'block_id': video_descriptor.scope_ids.usage_id.block_id,
'lang': lang
},
request=request,
)
for lang in transcript_langs
}
return {
"video_url": video_url,
"video_thumbnail_url": None,
"duration": duration,
"size": size,
"name": video_descriptor.display_name,
"transcripts": transcripts,
"language": video_descriptor.transcript_language,
"category": video_descriptor.category,
"id": unicode(video_descriptor.scope_ids.usage_id),
}

View File

@@ -0,0 +1,20 @@
from django.conf.urls import patterns, url, include
from django.conf import settings
from rest_framework import routers
from rest_framework.urlpatterns import format_suffix_patterns
from .views import VideoSummaryList, VideoTranscripts
urlpatterns = patterns('mobile_api.video_outlines.views',
url(
r'^courses/{}$'.format(settings.COURSE_ID_PATTERN),
VideoSummaryList.as_view(),
name='video-summary-list'
),
url(
r'^transcripts/{}/(?P<block_id>[^/]*)/(?P<lang>[^/]*)$'.format(settings.COURSE_ID_PATTERN),
VideoTranscripts.as_view(),
name='video-transcripts-detail'
),
)

View File

@@ -0,0 +1,86 @@
"""
Video Outlines
We only provide the listing view for a video outline, and video outlines are
only displayed at the course level. This is because it makes it a lot easier to
optimize and reason about, and it avoids having to tackle the bigger problem of
general XBlock representation in this rather specialized formatting.
"""
from functools import partial
from django.core.cache import cache
from django.http import Http404, HttpResponse
from rest_framework import generics, permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework.response import Response
from rest_framework.views import APIView
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import BlockUsageLocator
from courseware.access import has_access
from student.models import CourseEnrollment, User
from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import modulestore
from .serializers import BlockOutline, video_summary
class VideoSummaryList(generics.ListAPIView):
"""A list of all Videos in this Course that the user has access to."""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
def list(self, request, *args, **kwargs):
course_id = CourseKey.from_string(kwargs['course_id'])
course = get_mobile_course(course_id, request.user)
video_outline = list(
BlockOutline(
course_id,
course,
{"video": partial(video_summary, course)},
request,
)
)
return Response(video_outline)
class VideoTranscripts(generics.RetrieveAPIView):
"""Read-only view for a single transcript (SRT) file for a particular language.
Returns an `HttpResponse` with an SRT file download for the body.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, *args, **kwargs):
course_key = CourseKey.from_string(kwargs['course_id'])
block_id = kwargs['block_id']
lang = kwargs['lang']
usage_key = BlockUsageLocator(
course_key, block_type="video", block_id=block_id
)
try:
video_descriptor = modulestore().get_item(usage_key)
content, filename, mimetype = video_descriptor.get_transcript(lang=lang)
except (NotFoundError, ValueError, KeyError):
raise Http404("Transcript not found for {}, lang: {}".format(block_id, lang))
response = HttpResponse(content, content_type=mimetype)
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response
def get_mobile_course(course_id, user):
"""
Return only a CourseDescriptor if the course is mobile-ready or if the
requesting user is a staff member.
"""
course = modulestore().get_course(course_id, depth=None)
if course.mobile_available or has_access(user, 'staff', course):
return course
raise PermissionDenied(detail="Course not available on mobile.")

View File

@@ -287,6 +287,13 @@ FEATURES = {
# False to not redirect the user
'ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER': True,
# Expose Mobile REST API. Note that if you use this, you must also set
# ENABLE_OAUTH2_PROVIDER to True
'ENABLE_MOBILE_REST_API': False,
# Video Abstraction Layer used to allow video teams to manage video assets
# independently of courseware. https://github.com/edx/edx-val
'ENABLE_VIDEO_ABSTRACTION_LAYER_API': False,
}
# Ignore static asset files on import which match this pattern
@@ -1415,7 +1422,10 @@ INSTALLED_APPS = (
'edx_jsme', # Molecular Structure
# Country list
'django_countries'
'django_countries',
# edX Mobile API
'mobile_api',
)
######################### MARKETING SITE ###############################
@@ -1737,7 +1747,10 @@ OPTIONAL_APPS = (
'openassessment.assessment',
'openassessment.fileupload',
'openassessment.workflow',
'openassessment.xblock'
'openassessment.xblock',
# edxval
'edxval'
)
for app_name in OPTIONAL_APPS:

View File

@@ -211,6 +211,10 @@ FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
FEATURES['AUTH_USE_CERTIFICATES'] = False
########################### External REST APIs #################################
FEATURES['ENABLE_MOBILE_REST_API'] = True
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
################################# CELERY ######################################
# By default don't use a worker, execute tasks as if they were local functions

View File

@@ -48,7 +48,7 @@ ANALYTICS_DASHBOARD_URL = None
################################ DEBUG TOOLBAR ################################
INSTALLED_APPS += ('debug_toolbar',)
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',)
@@ -62,12 +62,13 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel',
'debug_toolbar_mongo.panel.MongoDebugPanel',
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance
# problems, but you shouldn't leave it on.
# 'debug_toolbar.panels.profiling.ProfilingPanel',
#'debug_toolbar.panels.profiling.ProfilingDebugPanel',
)
DEBUG_TOOLBAR_CONFIG = {
@@ -94,6 +95,10 @@ CC_PROCESSOR = {
}
}
########################### External REST APIs #################################
FEATURES['ENABLE_MOBILE_REST_API'] = True
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
#####################################################################
# See if the developer has any local overrides.
try:

View File

@@ -220,6 +220,10 @@ OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
############################## OAUTH2 Provider ################################
FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
########################### External REST APIs #################################
FEATURES['ENABLE_MOBILE_REST_API'] = True
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
###################### Payment ##############################3
# Enable fake payment processing page
FEATURES['ENABLE_PAYMENT_FAKE'] = True

View File

@@ -70,8 +70,15 @@ urlpatterns = ('', # nopep8
# Feedback Form endpoint
url(r'^submit_feedback$', 'util.views.submit_feedback'),
)
if settings.FEATURES["ENABLE_MOBILE_REST_API"]:
urlpatterns += (
url(r'^api/mobile/v0.5/', include('mobile_api.urls')),
url(r'^api/val/v0/', include('edxval.urls')),
)
# if settings.FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"):
urlpatterns += (
url(r'^verify_student/', include('verify_student.urls')),