Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
dca12d65
Commit
dca12d65
authored
Apr 11, 2017
by
Muhammad Ammar
Committed by
Mushtaq Ali
Jul 06, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Save or update course video image - TNL-6762
parent
7b910953
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
152 additions
and
53 deletions
+152
-53
cms/djangoapps/contentstore/tests/test_contentstore.py
+1
-1
cms/djangoapps/contentstore/views/tests/test_videos.py
+0
-0
cms/djangoapps/contentstore/views/videos.py
+97
-45
cms/envs/aws.py
+4
-0
cms/envs/common.py
+6
-0
cms/envs/test.py
+10
-0
cms/urls.py
+7
-0
common/lib/xmodule/xmodule/tests/test_video.py
+4
-2
common/lib/xmodule/xmodule/video_module/video_module.py
+4
-1
lms/djangoapps/courseware/tests/test_video_mongo.py
+4
-3
lms/envs/common.py
+14
-0
requirements/edx/github.txt
+1
-1
No files found.
cms/djangoapps/contentstore/tests/test_contentstore.py
View file @
dca12d65
...
...
@@ -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
=
[]
...
...
cms/djangoapps/contentstore/views/tests/test_videos.py
View file @
dca12d65
This diff is collapsed.
Click to expand it.
cms/djangoapps/contentstore/views/videos.py
View file @
dca12d65
"""
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
():
...
...
cms/envs/aws.py
View file @
dca12d65
...
...
@@ -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"
,
{})
...
...
cms/envs/common.py
View file @
dca12d65
...
...
@@ -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'
cms/envs/test.py
View file @
dca12d65
...
...
@@ -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'
cms/urls.py
View file @
dca12d65
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
+=
(
...
...
common/lib/xmodule/xmodule/tests/test_video.py
View file @
dca12d65
...
...
@@ -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
)
...
...
common/lib/xmodule/xmodule/video_module/video_module.py
View file @
dca12d65
...
...
@@ -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
...
...
lms/djangoapps/courseware/tests/test_video_mongo.py
View file @
dca12d65
...
...
@@ -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
)
...
...
lms/envs/common.py
View file @
dca12d65
...
...
@@ -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
...
...
requirements/edx/github.txt
View file @
dca12d65
...
...
@@ -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.1
3#egg=edxval==0.0.13
git+https://github.com/edx/edx-val.git@0.0.1
4#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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment