Commit dca12d65 by Muhammad Ammar Committed by Mushtaq Ali

Save or update course video image - TNL-6762

parent 7b910953
......@@ -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=[]
......
"""
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():
......
......@@ -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", {})
......
......@@ -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'
......@@ -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'
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<textbook_id>\d[^/]*)$'.format(settings.COURSE_KEY_PATTERN), 'textbooks_detail_handler'),
url(r'^videos/{}(?:/(?P<edx_video_id>[-\w]+))?$'.format(settings.COURSE_KEY_PATTERN), 'videos_handler'),
url(r'^video_images/{}(?:/(?P<edx_video_id>[-\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<group_configuration_id>\d+)(/)?(?P<group_id>\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 += (
......
......@@ -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)
......
......@@ -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
......
......@@ -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 = """
<video download_video="false" url_name="SampleProblem">
<video_asset client_video_id="test_client_video_id" duration="111.0">
<video_asset client_video_id="test_client_video_id" duration="111.0" image="">
<encoded_video profile="mobile" url="http://example.com/video" file_size="222" bitrate="333"/>
</video_asset>
</video>
......@@ -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)
......
......@@ -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
......
......@@ -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
......
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