diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 5dd616ebfc..8d4a9085fc 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1975,7 +1975,7 @@ class RerunCourseTest(ContentStoreTestCase): create_video( dict( edx_video_id="tree-hugger", - courses=[source_course.id], + courses=[unicode(source_course.id)], status='test', duration=2, encoded_videos=[] diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py index 6731bf6815..0b3ea1214f 100644 --- a/cms/djangoapps/contentstore/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/views/tests/test_videos.py @@ -19,11 +19,19 @@ from mock import Mock, patch from contentstore.models import VideoUploadConfig from contentstore.tests.utils import CourseTestCase from contentstore.utils import reverse_course_url +from contentstore.views.videos import ( + KEY_EXPIRATION_IN_SECONDS, + StatusDisplayStrings, + convert_video_status, + _get_default_video_image_url +) from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, StatusDisplayStrings, convert_video_status from xmodule.modulestore.tests.factories import CourseFactory +from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file -class VideoUploadTestMixin(object): + +class VideoUploadTestBase(object): """ Test cases for the video upload feature """ @@ -32,7 +40,7 @@ class VideoUploadTestMixin(object): return reverse_course_url(self.VIEW_NAME, course_key, kwargs) def setUp(self): - super(VideoUploadTestMixin, self).setUp() + super(VideoUploadTestBase, self).setUp() self.url = self.get_url_for_course_key(self.course.id) self.test_token = "test_token" self.course.video_upload_pipeline = { @@ -131,6 +139,11 @@ class VideoUploadTestMixin(object): if video["edx_video_id"] == edx_video_id ) + +class VideoUploadTestMixin(VideoUploadTestBase): + """ + Test cases for the video upload feature + """ def test_anon_user(self): self.client.logout() response = self.client.get(self.url) @@ -171,25 +184,25 @@ class VideoUploadTestMixin(object): class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): """Test cases for the main video upload endpoint""" - VIEW_NAME = "videos_handler" + VIEW_NAME = 'videos_handler' def test_get_json(self): response = self.client.get_json(self.url) self.assertEqual(response.status_code, 200) - response_videos = json.loads(response.content)["videos"] + response_videos = json.loads(response.content)['videos'] self.assertEqual(len(response_videos), len(self.previous_uploads)) for i, response_video in enumerate(response_videos): # Videos should be returned by creation date descending original_video = self.previous_uploads[-(i + 1)] self.assertEqual( set(response_video.keys()), - set(["edx_video_id", "client_video_id", "created", "duration", "status"]) + set(['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'course_video_image_url']) ) - dateutil.parser.parse(response_video["created"]) - for field in ["edx_video_id", "client_video_id", "duration"]: + dateutil.parser.parse(response_video['created']) + for field in ['edx_video_id', 'client_video_id', 'duration']: self.assertEqual(response_video[field], original_video[field]) self.assertEqual( - response_video["status"], + response_video['status'], convert_video_status(original_video) ) @@ -313,26 +326,26 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): response = json.loads(response.content) self.assertEqual(response['error'], 'The file name for %s must contain only ASCII characters.' % file_name) - @override_settings(AWS_ACCESS_KEY_ID="test_key_id", AWS_SECRET_ACCESS_KEY="test_secret") - @patch("boto.s3.key.Key") - @patch("boto.s3.connection.S3Connection") + @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret') + @patch('boto.s3.key.Key') + @patch('boto.s3.connection.S3Connection') def test_post_success(self, mock_conn, mock_key): files = [ { - "file_name": "first.mp4", - "content_type": "video/mp4", + 'file_name': 'first.mp4', + 'content_type': 'video/mp4', }, { - "file_name": "second.mp4", - "content_type": "video/mp4", + 'file_name': 'second.mp4', + 'content_type': 'video/mp4', }, { - "file_name": "third.mov", - "content_type": "video/quicktime", + 'file_name': 'third.mov', + 'content_type': 'video/quicktime', }, { - "file_name": "fourth.mp4", - "content_type": "video/mp4", + 'file_name': 'fourth.mp4', + 'content_type': 'video/mp4', }, ] @@ -341,7 +354,7 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): mock_key_instances = [ Mock( generate_url=Mock( - return_value="http://example.com/url_{}".format(file_info["file_name"]) + return_value='http://example.com/url_{}'.format(file_info['file_name']) ) ) for file_info in files @@ -351,14 +364,14 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): response = self.client.post( self.url, - json.dumps({"files": files}), - content_type="application/json" + json.dumps({'files': files}), + content_type='application/json' ) self.assertEqual(response.status_code, 200) response_obj = json.loads(response.content) mock_conn.assert_called_once_with(settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY) - self.assertEqual(len(response_obj["files"]), len(files)) + self.assertEqual(len(response_obj['files']), len(files)) self.assertEqual(mock_key.call_count, len(files)) for i, file_info in enumerate(files): # Ensure Key was set up correctly and extract id @@ -366,8 +379,8 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): self.assertEqual(key_call_args[0], bucket) path_match = re.match( ( - settings.VIDEO_UPLOAD_PIPELINE["ROOT_PATH"] + - "/([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$" + settings.VIDEO_UPLOAD_PIPELINE['ROOT_PATH'] + + '/([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$' ), key_call_args[1] ) @@ -375,32 +388,32 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): video_id = path_match.group(1) mock_key_instance = mock_key_instances[i] mock_key_instance.set_metadata.assert_any_call( - "course_video_upload_token", + 'course_video_upload_token', self.test_token ) mock_key_instance.set_metadata.assert_any_call( - "client_video_id", - file_info["file_name"] + 'client_video_id', + file_info['file_name'] ) - mock_key_instance.set_metadata.assert_any_call("course_key", unicode(self.course.id)) + mock_key_instance.set_metadata.assert_any_call('course_key', unicode(self.course.id)) mock_key_instance.generate_url.assert_called_once_with( KEY_EXPIRATION_IN_SECONDS, - "PUT", - headers={"Content-Type": file_info["content_type"]} + 'PUT', + headers={'Content-Type': file_info['content_type']} ) # Ensure VAL was updated val_info = get_video_info(video_id) - self.assertEqual(val_info["status"], "upload") - 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)]) + self.assertEqual(val_info['status'], 'upload') + 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): None}]) # Ensure response is correct - response_file = response_obj["files"][i] - self.assertEqual(response_file["file_name"], file_info["file_name"]) - self.assertEqual(response_file["upload_url"], mock_key_instance.generate_url()) + response_file = response_obj['files'][i] + self.assertEqual(response_file['file_name'], file_info['file_name']) + self.assertEqual(response_file['upload_url'], mock_key_instance.generate_url()) def _assert_video_removal(self, url, edx_video_id, deleted_videos): """ @@ -518,6 +531,84 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): self.assert_video_status(url, edx_video_id, 'Failed') +@patch.dict('django.conf.settings.FEATURES', {'ENABLE_VIDEO_UPLOAD_PIPELINE': True}) +@override_settings(VIDEO_UPLOAD_PIPELINE={'BUCKET': 'test_bucket', 'ROOT_PATH': 'test_root'}) +class VideoImageTestCase(VideoUploadTestBase, CourseTestCase): + """ + Tests for video image. + """ + + VIEW_NAME = "video_images_handler" + + def verify_image_upload_reponse(self, course_id, edx_video_id, upload_response): + """ + Verify that image is uploaded successfully. + + Arguments: + course_id: ID of course + edx_video_id: ID of video + upload_response: Upload response object + + Returns: + uploaded image url + """ + self.assertEqual(upload_response.status_code, 200) + response = json.loads(upload_response.content) + val_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=edx_video_id) + self.assertEqual(response['image_url'], val_image_url) + + return val_image_url + + def test_video_image(self): + """ + Test video image is saved. + """ + edx_video_id = 'test1' + video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) + with make_image_file() as image_file: + response = self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') + image_url1 = self.verify_image_upload_reponse(self.course.id, edx_video_id, response) + + # upload again to verify that new image is uploaded successfully + with make_image_file() as image_file: + response = self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') + image_url2 = self.verify_image_upload_reponse(self.course.id, edx_video_id, response) + + self.assertNotEqual(image_url1, image_url2) + + def test_video_image_no_file(self): + """ + Test that an error error message is returned if upload request is incorrect. + """ + video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': 'test1'}) + response = self.client.post(video_image_upload_url, {}) + self.assertEqual(response.status_code, 400) + response = json.loads(response.content) + self.assertEqual(response['error'], 'No file provided for video image') + + def test_default_video_image(self): + """ + Test default video image. + """ + edx_video_id = 'test1' + default_video_image_url = _get_default_video_image_url() + get_videos_url = reverse_course_url('videos_handler', self.course.id) + video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) + with make_image_file() as image_file: + self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') + + val_image_url = get_course_video_image_url(course_id=self.course.id, edx_video_id=edx_video_id) + + response = self.client.get_json(get_videos_url) + self.assertEqual(response.status_code, 200) + response_videos = json.loads(response.content)["videos"] + for response_video in response_videos: + if response_video['edx_video_id'] == edx_video_id: + self.assertEqual(response_video['course_video_image_url'], val_image_url) + else: + self.assertEqual(response_video['course_video_image_url'], default_video_image_url) + + @patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True}) @override_settings(VIDEO_UPLOAD_PIPELINE={"BUCKET": "test_bucket", "ROOT_PATH": "test_root"}) class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase): diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py index abd1d5c670..64e1d254fa 100644 --- a/cms/djangoapps/contentstore/views/videos.py +++ b/cms/djangoapps/contentstore/views/videos.py @@ -1,6 +1,11 @@ """ Views related to the video upload feature """ +from contextlib import closing +from datetime import datetime, timedelta +import logging + +from boto import s3 import csv import logging from datetime import datetime, timedelta @@ -10,17 +15,19 @@ import rfc6266 from boto import s3 from django.conf import settings from django.contrib.auth.decorators import login_required +from django.contrib.staticfiles.storage import staticfiles_storage from django.http import HttpResponse, HttpResponseNotFound from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop -from django.views.decorators.http import require_GET, require_http_methods +from django.views.decorators.http import require_GET, require_POST, require_http_methods from edxval.api import ( SortDirection, VideoSortField, create_video, get_videos_for_course, remove_video_for_course, - update_video_status + update_video_status, + update_video_image ) from opaque_keys.edx.keys import CourseKey @@ -31,7 +38,8 @@ from util.json_request import JsonResponse, expect_json from .course import get_course_and_check_access -__all__ = ["videos_handler", "video_encodings_download"] + +__all__ = ['videos_handler', 'video_encodings_download', 'video_images_handler'] LOGGER = logging.getLogger(__name__) @@ -145,6 +153,26 @@ def videos_handler(request, course_key_string, edx_video_id=None): return videos_post(course, request) +@expect_json +@login_required +@require_POST +def video_images_handler(request, course_key_string, edx_video_id=None): + if 'file' not in request.FILES: + return JsonResponse({"error": _(u'No file provided for video image')}, status=400) + + image_file = request.FILES['file'] + file_name = request.FILES['file'].name + + # TODO: Image file validation + with closing(image_file): + image_url = update_video_image(edx_video_id, course_key_string, image_file, file_name) + LOGGER.info( + 'VIDEOS: Video image uploaded for edx_video_id [%s] in course [%s]', edx_video_id, course_key_string + ) + + return JsonResponse({'image_url': image_url}) + + @login_required @require_GET def video_encodings_download(request, course_key_string): @@ -296,17 +324,39 @@ def _get_videos(course): return videos +def _get_default_video_image_url(): + """ + Returns default video image url + """ + return staticfiles_storage.url(settings.VIDEO_IMAGE_DEFAULT_FILENAME) + + def _get_index_videos(course): """ Returns the information about each video upload required for the video list """ - return list( - { - attr: video[attr] - for attr in ["edx_video_id", "client_video_id", "created", "duration", "status"] - } - for video in _get_videos(course) - ) + course_id = unicode(course.id) + default_video_image_url = _get_default_video_image_url() + attrs = ['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'courses'] + + def _get_values(video): + """ + Get data for predefined video attributes. + """ + values = {} + for attr in attrs: + if attr == 'courses': + course = filter(lambda c: course_id in c, video['courses']) + (__, image_url), = course[0].items() + values['course_video_image_url'] = image_url or default_video_image_url + else: + values[attr] = video[attr] + + return values + + return [ + _get_values(video) for video in _get_videos(course) + ] def videos_index_html(course): @@ -314,15 +364,16 @@ def videos_index_html(course): Returns an HTML page to display previous video uploads and allow new ones """ return render_to_response( - "videos_index.html", + 'videos_index.html', { - "context_course": course, - "video_handler_url": reverse_course_url("videos_handler", unicode(course.id)), - "encodings_download_url": reverse_course_url("video_encodings_download", unicode(course.id)), - "previous_uploads": _get_index_videos(course), - "concurrent_upload_limit": settings.VIDEO_UPLOAD_PIPELINE.get("CONCURRENT_UPLOAD_LIMIT", 0), - "video_supported_file_formats": VIDEO_SUPPORTED_FILE_FORMATS.keys(), - "video_upload_max_file_size": VIDEO_UPLOAD_MAX_FILE_SIZE_GB + 'context_course': course, + 'image_upload_url': reverse_course_url('video_images_handler', unicode(course.id)), + 'video_handler_url': reverse_course_url('videos_handler', unicode(course.id)), + 'encodings_download_url': reverse_course_url('video_encodings_download', unicode(course.id)), + 'previous_uploads': _get_index_videos(course), + 'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0), + 'video_supported_file_formats': VIDEO_SUPPORTED_FILE_FORMATS.keys(), + 'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB } ) @@ -331,12 +382,13 @@ def videos_index_json(course): """ Returns JSON in the following format: { - "videos": [{ - "edx_video_id": "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa", - "client_video_id": "video.mp4", - "created": "1970-01-01T00:00:00Z", - "duration": 42.5, - "status": "upload" + 'videos': [{ + 'edx_video_id': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa', + 'client_video_id': 'video.mp4', + 'created': '1970-01-01T00:00:00Z', + 'duration': 42.5, + 'status': 'upload', + 'course_video_image_url': 'https://video/images/1234.jpg' }] } """ @@ -364,29 +416,29 @@ def videos_post(course, request): The returned array corresponds exactly to the input array. """ error = None - if "files" not in request.json: + if 'files' not in request.json: error = "Request object is not JSON or does not contain 'files'" elif any( - "file_name" not in file or "content_type" not in file - for file in request.json["files"] + 'file_name' not in file or 'content_type' not in file + for file in request.json['files'] ): error = "Request 'files' entry does not contain 'file_name' and 'content_type'" elif any( file['content_type'] not in VIDEO_SUPPORTED_FILE_FORMATS.values() - for file in request.json["files"] + for file in request.json['files'] ): error = "Request 'files' entry contain unsupported content_type" if error: - return JsonResponse({"error": error}, status=400) + return JsonResponse({'error': error}, status=400) bucket = storage_service_bucket() - course_video_upload_token = course.video_upload_pipeline["course_video_upload_token"] - req_files = request.json["files"] + course_video_upload_token = course.video_upload_pipeline['course_video_upload_token'] + req_files = request.json['files'] resp_files = [] for req_file in req_files: - file_name = req_file["file_name"] + file_name = req_file['file_name'] try: file_name.encode('ascii') @@ -397,30 +449,30 @@ def videos_post(course, request): edx_video_id = unicode(uuid4()) key = storage_service_key(bucket, file_name=edx_video_id) for metadata_name, value in [ - ("course_video_upload_token", course_video_upload_token), - ("client_video_id", file_name), - ("course_key", unicode(course.id)), + ('course_video_upload_token', course_video_upload_token), + ('client_video_id', file_name), + ('course_key', unicode(course.id)), ]: key.set_metadata(metadata_name, value) upload_url = key.generate_url( KEY_EXPIRATION_IN_SECONDS, - "PUT", - headers={"Content-Type": req_file["content_type"]} + 'PUT', + headers={'Content-Type': req_file['content_type']} ) # persist edx_video_id in VAL create_video({ - "edx_video_id": edx_video_id, - "status": "upload", - "client_video_id": file_name, - "duration": 0, - "encoded_videos": [], - "courses": [course.id] + 'edx_video_id': edx_video_id, + 'status': 'upload', + 'client_video_id': file_name, + 'duration': 0, + 'encoded_videos': [], + 'courses': [unicode(course.id)] }) - resp_files.append({"file_name": file_name, "upload_url": upload_url, "edx_video_id": edx_video_id}) + resp_files.append({'file_name': file_name, 'upload_url': upload_url, 'edx_video_id': edx_video_id}) - return JsonResponse({"files": resp_files}, status=200) + return JsonResponse({'files': resp_files}, status=200) def storage_service_bucket(): diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 1474eca80a..4bc4ad553d 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -440,6 +440,10 @@ ADVANCED_PROBLEM_TYPES = ENV_TOKENS.get('ADVANCED_PROBLEM_TYPES', ADVANCED_PROBL VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIPELINE) +################ VIDEO IMAGE STORAGE ############### + +VIDEO_IMAGE_SETTINGS = ENV_TOKENS.get('VIDEO_IMAGE_SETTINGS', VIDEO_IMAGE_SETTINGS) + ################ PUSH NOTIFICATIONS ############### PARSE_KEYS = AUTH_TOKENS.get("PARSE_KEYS", {}) diff --git a/cms/envs/common.py b/cms/envs/common.py index 47cc327272..484d216db9 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -103,6 +103,8 @@ from lms.envs.common import ( CONTACT_EMAIL, DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH, + # Video Image settings + VIDEO_IMAGE_SETTINGS, ) from path import Path as path from warnings import simplefilter @@ -1344,3 +1346,7 @@ PROFILE_IMAGE_SIZES_MAP = { 'medium': 50, 'small': 30 } + +###################### VIDEO IMAGE STORAGE ###################### + +VIDEO_IMAGE_DEFAULT_FILENAME = 'default_video_image.png' diff --git a/cms/envs/test.py b/cms/envs/test.py index 88de0a47af..332a369891 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -335,3 +335,13 @@ FEATURES['CUSTOM_COURSES_EDX'] = True # API access management -- needed for simple-history to run. INSTALLED_APPS += ('openedx.core.djangoapps.api_admin',) + +########################## VIDEO IMAGE STORAGE ############################ +VIDEO_IMAGE_SETTINGS = dict( + STORAGE_KWARGS=dict( + location=MEDIA_ROOT, + base_url=MEDIA_URL, + ), + DIRECTORY_PREFIX='videoimage/', +) +VIDEO_IMAGE_DEFAULT_FILENAME = 'default_video_image.png' diff --git a/cms/urls.py b/cms/urls.py index 08636d7e67..a7afed402e 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -1,5 +1,6 @@ from django.conf import settings from django.conf.urls import include, patterns, url +from django.conf.urls.static import static # There is a course creators admin table. from ratelimitbackend import admin @@ -112,6 +113,7 @@ urlpatterns += patterns( url(r'^textbooks/{}$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_list_handler'), url(r'^textbooks/{}/(?P\d[^/]*)$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_detail_handler'), url(r'^videos/{}(?:/(?P[-\w]+))?$'.format(settings.COURSE_KEY_PATTERN), 'videos_handler'), + url(r'^video_images/{}(?:/(?P[-\w]+))?$'.format(settings.COURSE_KEY_PATTERN), 'video_images_handler'), url(r'^video_encodings_download/{}$'.format(settings.COURSE_KEY_PATTERN), 'video_encodings_download'), url(r'^group_configurations/{}$'.format(settings.COURSE_KEY_PATTERN), 'group_configurations_list_handler'), url(r'^group_configurations/{}/(?P\d+)(/)?(?P\d+)?$'.format( @@ -189,6 +191,11 @@ if settings.DEBUG: except ImportError: pass + urlpatterns += static( + settings.VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['base_url'], + document_root=settings.VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location'] + ) + if 'debug_toolbar' in settings.INSTALLED_APPS: import debug_toolbar urlpatterns += ( diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py index e38f11bd04..51c43006b1 100644 --- a/common/lib/xmodule/xmodule/tests/test_video.py +++ b/common/lib/xmodule/xmodule/tests/test_video.py @@ -18,7 +18,7 @@ import datetime from uuid import uuid4 from lxml import etree -from mock import ANY, Mock, patch +from mock import ANY, Mock, patch, MagicMock import ddt from django.conf import settings @@ -673,7 +673,7 @@ class VideoExportTestCase(VideoDescriptorTestBase): """ Test that we write the correct XML on export. """ - def mock_val_export(edx_video_id): + def mock_val_export(edx_video_id, course_id): """Mock edxval.api.export_to_xml""" return etree.Element( 'video_asset', @@ -695,6 +695,7 @@ class VideoExportTestCase(VideoDescriptorTestBase): self.descriptor.download_video = True self.descriptor.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'} self.descriptor.edx_video_id = 'test_edx_video_id' + self.descriptor.runtime.course_id = MagicMock() xml = self.descriptor.definition_to_xml(None) # We don't use the `resource_fs` parameter parser = etree.XMLParser(remove_blank_text=True) @@ -718,6 +719,7 @@ class VideoExportTestCase(VideoDescriptorTestBase): mock_val_api.ValVideoNotFoundError = _MockValVideoNotFoundError mock_val_api.export_to_xml = Mock(side_effect=mock_val_api.ValVideoNotFoundError) self.descriptor.edx_video_id = 'test_edx_video_id' + self.descriptor.runtime.course_id = MagicMock() xml = self.descriptor.definition_to_xml(None) parser = etree.XMLParser(remove_blank_text=True) diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 4cdcfbb47f..46f3602db8 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -653,7 +653,10 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler if self.edx_video_id and edxval_api: try: - xml.append(edxval_api.export_to_xml(self.edx_video_id)) + xml.append(edxval_api.export_to_xml( + self.edx_video_id, + unicode(self.runtime.course_id.for_branch(None))) + ) except edxval_api.ValVideoNotFoundError: pass diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index 266a97bbc1..67a008154e 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -1261,7 +1261,7 @@ class TestVideoDescriptorStudentViewJson(TestCase): 'duration': self.TEST_DURATION, 'status': 'dummy', 'encoded_videos': [self.TEST_ENCODED_VIDEO], - 'courses': [self.video.location.course_key] if associate_course_in_val else [], + 'courses': [unicode(self.video.location.course_key)] if associate_course_in_val else [], }) self.val_video = get_video_info(self.TEST_EDX_VIDEO_ID) # pylint: disable=attribute-defined-outside-init @@ -1391,6 +1391,7 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase): def setUp(self): super(VideoDescriptorTest, self).setUp() self.descriptor.runtime.handler_url = MagicMock() + self.descriptor.runtime.course_id = MagicMock() def test_get_context(self): """" @@ -1438,7 +1439,7 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase): actual = self.descriptor.definition_to_xml(resource_fs=None) expected_str = """ @@ -1474,7 +1475,7 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase): self.assertEqual(video_data['client_video_id'], 'test_client_video_id') self.assertEqual(video_data['duration'], 111) self.assertEqual(video_data['status'], 'imported') - self.assertEqual(video_data['courses'], [id_generator.target_course_id]) + self.assertEqual(video_data['courses'], [{id_generator.target_course_id: None}]) self.assertEqual(video_data['encoded_videos'][0]['profile'], 'mobile') self.assertEqual(video_data['encoded_videos'][0]['url'], 'http://example.com/video') self.assertEqual(video_data['encoded_videos'][0]['file_size'], 222) diff --git a/lms/envs/common.py b/lms/envs/common.py index d6f240dff3..eeb71ada91 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2564,6 +2564,20 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60 TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC' +########################## VIDEO IMAGE STORAGE ############################ + +VIDEO_IMAGE_SETTINGS = dict( + # Backend storage + # STORAGE_CLASS='storages.backends.s3boto.S3BotoStorage', + # STORAGE_KWARGS=dict(bucket='video-image-bucket'), + STORAGE_KWARGS=dict( + location=MEDIA_ROOT, + base_url=MEDIA_URL, + ), + DIRECTORY_PREFIX='videoimage/', +) + + # Source: # http://loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt according to http://en.wikipedia.org/wiki/ISO_639-1 # Note that this is used as the set of choices to the `code` field of the diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 258feafa8e..128795f69a 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -77,7 +77,7 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002 git+https://github.com/edx/edx-ora2.git@1.4.3#egg=ora2==1.4.3 -e git+https://github.com/edx/edx-submissions.git@2.0.0#egg=edx-submissions==2.0.0 git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3 -git+https://github.com/edx/edx-val.git@0.0.13#egg=edxval==0.0.13 +git+https://github.com/edx/edx-val.git@0.0.14#egg=edxval==0.0.14 git+https://github.com/pmitros/RecommenderXBlock.git@v1.2#egg=recommender-xblock==1.2 git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1 -e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock