Commit 44235108 by Christopher Lee

Merge pull request #7480 from edx/clee/MA-169_MA-77

Clee/ma 169 ma 77
parents c2dcbd70 e8dfac09
......@@ -14,6 +14,8 @@ from course_action_state.models import CourseRerunState
from contentstore.utils import initialize_permissions
from opaque_keys.edx.keys import CourseKey
from edxval.api import copy_course_videos
@task()
def rerun_course(source_course_key_string, destination_course_key_string, user_id, fields=None):
......@@ -37,6 +39,10 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
# update state: Succeeded
CourseRerunState.objects.succeeded(course_key=destination_course_key)
# call edxval to attach videos to the rerun
copy_course_videos(source_course_key, destination_course_key)
return "succeeded"
except DuplicateCourseError as exc:
......
......@@ -25,6 +25,8 @@ from openedx.core.lib.tempdir import mkdtemp_clean
from contentstore.tests.utils import parse_json, AjaxEnabledTestClient, CourseTestCase
from contentstore.views.component import ADVANCED_COMPONENT_TYPES
from edxval.api import create_video, get_videos_for_course
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.utils import restore_asset_from_trashcan, empty_asset_trashcan
from xmodule.exceptions import InvalidVersionError
......@@ -1712,11 +1714,37 @@ class RerunCourseTest(ContentStoreTestCase):
self.assertInCourseListing(source_course_key)
self.assertInCourseListing(destination_course_key)
def test_rerun_course_no_videos_in_val(self):
"""
Test when rerunning a course with no videos, VAL copies nothing
"""
source_course = CourseFactory.create()
destination_course_key = self.post_rerun_request(source_course.id)
self.verify_rerun_course(source_course.id, destination_course_key, self.destination_course_data['display_name'])
videos = list(get_videos_for_course(destination_course_key))
self.assertEqual(0, len(videos))
self.assertInCourseListing(destination_course_key)
def test_rerun_course_success(self):
source_course = CourseFactory.create()
create_video(
dict(
edx_video_id="tree-hugger",
courses=[source_course.id],
status='test',
duration=2,
encoded_videos=[]
)
)
destination_course_key = self.post_rerun_request(source_course.id)
self.verify_rerun_course(source_course.id, destination_course_key, self.destination_course_data['display_name'])
# Verify that the VAL copies videos to the rerun
source_videos = list(get_videos_for_course(source_course.id))
target_videos = list(get_videos_for_course(destination_course_key))
self.assertEqual(1, len(source_videos))
self.assertEqual(source_videos, target_videos)
def test_rerun_of_rerun(self):
source_course = CourseFactory.create()
rerun_course_key = self.post_rerun_request(source_course.id)
......
......@@ -315,6 +315,7 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
self.assertEqual(val_info["client_video_id"], file_info["file_name"])
self.assertEqual(val_info["status"], "upload")
self.assertEqual(val_info["duration"], 0)
self.assertEqual(val_info["courses"], [unicode(self.course.id)])
# Ensure response is correct
response_file = response_obj["files"][i]
......
......@@ -341,6 +341,7 @@ def videos_post(course, request):
"client_video_id": file_name,
"duration": 0,
"encoded_videos": [],
"courses": [course.id]
})
resp_files.append({"file_name": file_name, "upload_url": upload_url})
......
"""
Django admin dashboard configuration for LMS XBlock infrastructure.
"""
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from mobile_api.models import MobileApiConfig
admin.site.register(MobileApiConfig, ConfigurationModelAdmin)
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'MobileApiConfig'
db.create_table('mobile_api_mobileapiconfig', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
('video_profiles', self.gf('django.db.models.fields.TextField')(blank=True)),
))
db.send_create_signal('mobile_api', ['MobileApiConfig'])
if not db.dry_run:
orm.MobileApiConfig.objects.create(
video_profiles="mobile_low,mobile_high,youtube",
)
def backwards(self, orm):
# Deleting model 'MobileApiConfig'
db.delete_table('mobile_api_mobileapiconfig')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'mobile_api.mobileapiconfig': {
'Meta': {'object_name': 'MobileApiConfig'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'video_profiles': ('django.db.models.fields.TextField', [], {'blank': 'True'})
}
}
complete_apps = ['mobile_api']
"""
A models.py is required to make this an app (until we move to Django 1.7)
ConfigurationModel for the mobile_api djangoapp.
"""
from django.db.models.fields import TextField
from config_models.models import ConfigurationModel
class MobileApiConfig(ConfigurationModel):
"""
Configuration for the video upload feature.
The order in which the comma-separated list of names of profiles are given
is in priority order.
"""
video_profiles = TextField(
blank=True,
help_text="A comma-separated list of names of profiles to include for videos returned from the mobile API."
)
@classmethod
def get_video_profiles(cls):
"""
Get the list of profiles in priority order when requesting from VAL
"""
return [profile.strip() for profile in cls.current().video_profiles.split(",") if profile] # pylint: disable=no-member
# -*- coding: utf-8 -*-
"""
Tests for mobile API utilities.
"""
import ddt
from django.test import TestCase
from mobile_api.models import MobileApiConfig
from .utils import mobile_course_access, mobile_view
......@@ -25,3 +27,33 @@ class TestMobileAPIDecorators(TestCase):
self.assertIn("Test docstring of decorated function.", decorated_func.__doc__)
self.assertEquals(decorated_func.__name__, "decorated_func")
self.assertTrue(decorated_func.__module__.endswith("tests"))
class TestMobileApiConfig(TestCase):
"""
Tests MobileAPIConfig
"""
def test_video_profile_list(self):
"""Check that video_profiles config is returned in order as a list"""
MobileApiConfig(video_profiles="mobile_low,mobile_high,youtube").save()
video_profile_list = MobileApiConfig.get_video_profiles()
self.assertEqual(
video_profile_list,
[u'mobile_low', u'mobile_high', u'youtube']
)
def test_video_profile_list_with_whitespace(self):
"""Check video_profiles config with leading and trailing whitespace"""
MobileApiConfig(video_profiles=" mobile_low , mobile_high,youtube ").save()
video_profile_list = MobileApiConfig.get_video_profiles()
self.assertEqual(
video_profile_list,
[u'mobile_low', u'mobile_high', u'youtube']
)
def test_empty_video_profile(self):
"""Test an empty video_profile"""
MobileApiConfig(video_profiles="").save()
video_profile_list = MobileApiConfig.get_video_profiles()
self.assertEqual(video_profile_list, [])
......@@ -10,7 +10,7 @@ from courseware.module_render import get_module_for_descriptor
from util.module_utils import get_dynamic_descriptor_children
from edxval.api import (
get_video_info_for_course_and_profile, ValInternalError
get_video_info_for_course_and_profiles, ValInternalError
)
......@@ -18,7 +18,7 @@ class BlockOutline(object):
"""
Serializes course videos, pulling data from VAL and the video modules.
"""
def __init__(self, course_id, start_block, block_types, request):
def __init__(self, course_id, start_block, block_types, request, video_profiles):
"""Create a BlockOutline using `start_block` as a starting point."""
self.start_block = start_block
self.block_types = block_types
......@@ -26,8 +26,8 @@ class BlockOutline(object):
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"
self.local_cache['course_videos'] = get_video_info_for_course_and_profiles(
unicode(course_id), video_profiles
)
except ValInternalError: # pragma: nocover
self.local_cache['course_videos'] = {}
......@@ -159,7 +159,7 @@ def find_urls(course_id, block, child_to_parent, request):
return unit_url, section_url
def video_summary(course, course_id, video_descriptor, request, local_cache):
def video_summary(video_profiles, course_id, video_descriptor, request, local_cache):
"""
returns summary dict for the given video module
"""
......@@ -182,19 +182,29 @@ def video_summary(course, course_id, video_descriptor, request, local_cache):
ret.update(always_available_data)
return ret
# 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']
# Get encoded videos
video_data = local_cache['course_videos'].get(video_descriptor.edx_video_id, {})
# Get highest priority video to populate backwards compatible field
default_encoded_video = {}
if video_data:
for profile in video_profiles:
default_encoded_video = video_data['profiles'].get(profile, {})
if default_encoded_video:
break
if default_encoded_video:
video_url = default_encoded_video['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)
# Get duration/size, else default
duration = video_data.get('duration', None)
size = default_encoded_video.get('file_size', 0)
# Transcripts...
transcript_langs = video_descriptor.available_translations(verify_assets=False)
......@@ -219,6 +229,7 @@ def video_summary(course, course_id, video_descriptor, request, local_cache):
"size": size,
"transcripts": transcripts,
"language": video_descriptor.get_default_transcript_language(),
"encoded_videos": video_data.get('profiles')
}
ret.update(always_available_data)
return ret
......@@ -9,6 +9,7 @@ from uuid import uuid4
from collections import namedtuple
from edxval import api
from mobile_api.models import MobileApiConfig
from xmodule.modulestore.tests.factories import ItemFactory
from xmodule.video_module import transcripts_utils
from xmodule.modulestore.django import modulestore
......@@ -58,6 +59,8 @@ class TestVideoAPITestCase(MobileAPITestCase):
self.edx_video_id = 'testing-123'
self.video_url = 'http://val.edx.org/val/video.mp4'
self.video_url_high = 'http://val.edx.org/val/video_high.mp4'
self.youtube_url = 'http://val.edx.org/val/youtube.mp4'
self.html5_video_url = 'http://video.edx.org/html5/video.mp4'
api.create_profile({
......@@ -67,6 +70,12 @@ class TestVideoAPITestCase(MobileAPITestCase):
'height': 720
})
api.create_profile({
'profile_name': 'mobile_high',
'extension': 'mp4',
'width': 750,
'height': 590
})
api.create_profile({
'profile_name': 'mobile_low',
'extension': 'mp4',
'width': 640,
......@@ -92,9 +101,19 @@ class TestVideoAPITestCase(MobileAPITestCase):
'url': self.video_url,
'file_size': 12345,
'bitrate': 250
}
},
{
'profile': 'mobile_high',
'url': self.video_url_high,
'file_size': 99999,
'bitrate': 250
},
]})
# Set requested profiles
MobileApiConfig(video_profiles="mobile_low,mobile_high,youtube").save()
class TestVideoAPIMixin(object):
"""
......@@ -450,6 +469,112 @@ class TestVideoSummaryList(
self.assertEqual(course_outline[0]["summary"]["category"], "video")
self.assertTrue(course_outline[0]["summary"]["only_on_web"])
def test_mobile_api_config(self):
"""
Tests VideoSummaryList with different MobileApiConfig video_profiles
"""
self.login_and_enroll()
edx_video_id = "testing_mobile_high"
api.create_video({
'edx_video_id': edx_video_id,
'status': 'test',
'client_video_id': u"test video omega \u03a9",
'duration': 12,
'courses': [unicode(self.course.id)],
'encoded_videos': [
{
'profile': 'youtube',
'url': self.youtube_url,
'file_size': 2222,
'bitrate': 4444
},
{
'profile': 'mobile_high',
'url': self.video_url_high,
'file_size': 111,
'bitrate': 333
},
]})
ItemFactory.create(
parent=self.other_unit,
category="video",
display_name=u"testing mobile high video",
edx_video_id=edx_video_id,
)
expected_output = {
'category': u'video',
'video_thumbnail_url': None,
'language': u'en',
'name': u'testing mobile high video',
'video_url': self.video_url_high,
'duration': 12.0,
'transcripts': {
'en': 'http://testserver/api/mobile/v0.5/video_outlines/transcripts/{}/testing_mobile_high_video/en'.format(self.course.id) # pylint: disable=line-too-long
},
'only_on_web': False,
'encoded_videos': {
u'mobile_high': {
'url': self.video_url_high,
'file_size': 111
},
u'youtube': {
'url': self.youtube_url,
'file_size': 2222
}
},
'size': 111
}
# Testing when video_profiles='mobile_low,mobile_high,youtube'
course_outline = self.api_response().data
course_outline[0]['summary'].pop("id")
self.assertEqual(course_outline[0]['summary'], expected_output)
# Testing when there is no mobile_low, and that mobile_high doesn't show
MobileApiConfig(video_profiles="mobile_low,youtube").save()
course_outline = self.api_response().data
expected_output['encoded_videos'].pop('mobile_high')
expected_output['video_url'] = self.youtube_url
expected_output['size'] = 2222
course_outline[0]['summary'].pop("id")
self.assertEqual(course_outline[0]['summary'], expected_output)
# Testing where youtube is the default video over mobile_high
MobileApiConfig(video_profiles="youtube,mobile_high").save()
course_outline = self.api_response().data
expected_output['encoded_videos']['mobile_high'] = {
'url': self.video_url_high,
'file_size': 111
}
course_outline[0]['summary'].pop("id")
self.assertEqual(course_outline[0]['summary'], expected_output)
def test_video_not_in_val(self):
self.login_and_enroll()
self._create_video_with_subs()
ItemFactory.create(
parent=self.other_unit,
category="video",
edx_video_id="some_non_existent_id_in_val",
display_name=u"some non existent video in val",
html5_sources=[self.html5_video_url]
)
summary = self.api_response().data[1]['summary']
self.assertEqual(summary['name'], "some non existent video in val")
self.assertIsNone(summary['encoded_videos'])
self.assertIsNone(summary['duration'])
self.assertEqual(summary['size'], 0)
self.assertEqual(summary['video_url'], self.html5_video_url)
def test_course_list(self):
self.login_and_enroll()
self._create_video_with_subs()
......@@ -488,7 +613,6 @@ class TestVideoSummaryList(
self.assertFalse(course_outline[1]['summary']['only_on_web'])
self.assertEqual(course_outline[1]['path'][2]['name'], self.other_unit.display_name)
self.assertEqual(course_outline[1]['path'][2]['id'], unicode(self.other_unit.location))
self.assertEqual(course_outline[2]['summary']['video_url'], self.html5_video_url)
self.assertEqual(course_outline[2]['summary']['size'], 0)
self.assertFalse(course_outline[2]['summary']['only_on_web'])
......
......@@ -9,6 +9,7 @@ general XBlock representation in this rather specialized formatting.
from functools import partial
from django.http import Http404, HttpResponse
from mobile_api.models import MobileApiConfig
from rest_framework import generics
from rest_framework.response import Response
......@@ -78,12 +79,14 @@ class VideoSummaryList(generics.ListAPIView):
@mobile_course_access(depth=None)
def list(self, request, course, *args, **kwargs):
video_profiles = MobileApiConfig.get_video_profiles()
video_outline = list(
BlockOutline(
course.id,
course,
{"video": partial(video_summary, course)},
{"video": partial(video_summary, video_profiles)},
request,
video_profiles,
)
)
return Response(video_outline)
......
......@@ -35,7 +35,7 @@ git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a
-e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease
-e git+https://github.com/edx/i18n-tools.git@193cebd9aa784f8899ef496f2aa050b08eff402b#egg=i18n-tools
-e git+https://github.com/edx/edx-oauth2-provider.git@0.4.2#egg=oauth2-provider
-e git+https://github.com/edx/edx-val.git@fbec6efc86abb36f55de947baacc2092881dcde2#egg=edx-val
-e git+https://github.com/edx/edx-val.git@64aa7637e3459fb3000a85a9e156880a40307dd1#egg=edx-val
-e git+https://github.com/pmitros/RecommenderXBlock.git@9b07e807c89ba5761827d0387177f71aa57ef056#egg=recommender-xblock
-e git+https://github.com/edx/edx-milestones.git@547f2250ee49e73ce8d7ff4e78ecf1b049892510#egg=edx-milestones
-e git+https://github.com/edx/edx-search.git@21ac6b06b3bfe789dcaeaf4e2ab5b00a688324d4#egg=edx-search
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment