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
14b8c12e
Commit
14b8c12e
authored
Sep 09, 2017
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added Studio API endpoint to update course image
LEARNER-2468
parent
317f719d
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
183 additions
and
70 deletions
+183
-70
cms/djangoapps/api/v1/serializers/course_runs.py
+42
-1
cms/djangoapps/api/v1/tests/test_serializers/test_course_runs.py
+9
-3
cms/djangoapps/api/v1/tests/test_views/test_course_runs.py
+48
-6
cms/djangoapps/api/v1/views/course_runs.py
+20
-2
cms/djangoapps/contentstore/views/assets.py
+53
-58
cms/djangoapps/contentstore/views/exception.py
+7
-0
common/lib/xmodule/xmodule/modulestore/mixed.py
+4
-0
No files found.
cms/djangoapps/api/v1/serializers/course_runs.py
View file @
14b8c12e
...
...
@@ -2,12 +2,18 @@ import six
from
django.contrib.auth
import
get_user_model
from
django.db
import
transaction
from
rest_framework
import
serializers
from
rest_framework.fields
import
get_attribute
from
rest_framework.fields
import
empty
from
cms.djangoapps.contentstore.views.course
import
create_new_course
,
get_course_and_check_access
,
rerun_course
from
contentstore.views.assets
import
update_course_run_asset
from
openedx.core.lib.courses
import
course_image_url
from
student.models
import
CourseAccessRole
from
xmodule.modulestore.django
import
modulestore
IMAGE_TYPES
=
{
'image/jpeg'
:
'jpg'
,
'image/png'
:
'png'
,
}
User
=
get_user_model
()
...
...
@@ -53,10 +59,45 @@ class CourseRunTeamSerializerMixin(serializers.Serializer):
])
def
image_is_jpeg_or_png
(
value
):
content_type
=
value
.
content_type
if
content_type
not
in
IMAGE_TYPES
.
keys
():
raise
serializers
.
ValidationError
(
'Only JPEG and PNG image types are supported. {} is not valid'
.
format
(
content_type
))
class
CourseRunImageField
(
serializers
.
ImageField
):
default_validators
=
[
image_is_jpeg_or_png
]
def
get_attribute
(
self
,
instance
):
return
course_image_url
(
instance
)
def
to_representation
(
self
,
value
):
# Value will always be the URL path of the image.
request
=
self
.
context
[
'request'
]
return
request
.
build_absolute_uri
(
value
)
class
CourseRunImageSerializer
(
serializers
.
Serializer
):
# We set an empty default to prevent the parent serializer from attempting
# to save this value to the Course object.
card_image
=
CourseRunImageField
(
source
=
'course_image'
,
default
=
empty
)
def
update
(
self
,
instance
,
validated_data
):
course_image
=
validated_data
[
'course_image'
]
course_image
.
name
=
'course_image.'
+
IMAGE_TYPES
[
course_image
.
content_type
]
update_course_run_asset
(
instance
.
id
,
course_image
)
instance
.
course_image
=
course_image
.
name
modulestore
()
.
update_item
(
instance
,
self
.
context
[
'request'
]
.
user
.
id
)
return
instance
class
CourseRunSerializer
(
CourseRunTeamSerializerMixin
,
serializers
.
Serializer
):
id
=
serializers
.
CharField
(
read_only
=
True
)
title
=
serializers
.
CharField
(
source
=
'display_name'
)
schedule
=
CourseRunScheduleSerializer
(
source
=
'*'
,
required
=
False
)
images
=
CourseRunImageSerializer
(
source
=
'*'
,
required
=
False
)
def
update
(
self
,
instance
,
validated_data
):
team
=
validated_data
.
pop
(
'team'
,
[])
...
...
cms/djangoapps/api/v1/tests/test_serializers/test_course_runs.py
View file @
14b8c12e
import
datetime
import
pytz
from
django.test
import
RequestFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
openedx.core.lib.courses
import
course_image_url
from
student.roles
import
CourseInstructorRole
,
CourseStaffRole
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
..utils
import
serialize_datetime
from
...serializers.course_runs
import
CourseRunSerializer
...
...
@@ -23,7 +25,8 @@ class CourseRunSerializerTests(ModuleStoreTestCase):
staff
=
UserFactory
()
CourseStaffRole
(
course
.
id
)
.
add_users
(
staff
)
serializer
=
CourseRunSerializer
(
course
)
request
=
RequestFactory
()
.
get
(
''
)
serializer
=
CourseRunSerializer
(
course
,
context
=
{
'request'
:
request
})
expected
=
{
'id'
:
str
(
course
.
id
),
'title'
:
course
.
display_name
,
...
...
@@ -43,5 +46,8 @@ class CourseRunSerializerTests(ModuleStoreTestCase):
'role'
:
'staff'
,
},
],
'images'
:
{
'card_image'
:
request
.
build_absolute_uri
(
course_image_url
(
course
)),
}
}
assert
serializer
.
data
==
expected
cms/djangoapps/api/v1/tests/test_views/test_course_runs.py
View file @
14b8c12e
import
datetime
import
pytz
from
django.core.files.uploadedfile
import
SimpleUploadedFile
from
django.core.urlresolvers
import
reverse
from
django.test
import
RequestFactory
from
opaque_keys.edx.keys
import
CourseKey
from
rest_framework.test
import
APIClient
from
student.models
import
CourseAccessRol
e
from
student.tests.factories
import
AdminFactory
,
TEST_PASSWORD
,
UserFactory
from
xmodule.contentstore.content
import
StaticContent
from
xmodule.contentstore.django
import
contentstor
e
from
xmodule.exceptions
import
NotFoundError
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ToyCourseFactory
from
openedx.core.lib.courses
import
course_image_url
from
student.models
import
CourseAccessRole
from
student.tests.factories
import
AdminFactory
,
TEST_PASSWORD
,
UserFactory
from
..utils
import
serialize_datetime
from
...serializers.course_runs
import
CourseRunSerializer
...
...
@@ -34,6 +40,9 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
CourseAccessRole
.
objects
.
get
(
course_id
=
course_run
.
id
,
user
=
user
,
role
=
role
)
assert
CourseAccessRole
.
objects
.
filter
(
course_id
=
course_run
.
id
)
.
count
()
==
1
def
get_serializer_context
(
self
):
return
{
'request'
:
RequestFactory
()
.
get
(
''
)}
def
test_without_authentication
(
self
):
self
.
client
.
logout
()
response
=
self
.
client
.
get
(
self
.
list_url
)
...
...
@@ -53,14 +62,14 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
# Order matters for the assertion
course_runs
=
sorted
(
course_runs
,
key
=
lambda
course_run
:
str
(
course_run
.
id
))
actual
=
sorted
(
response
.
data
,
key
=
lambda
course_run
:
course_run
[
'id'
])
assert
actual
==
CourseRunSerializer
(
course_runs
,
many
=
True
)
.
data
assert
actual
==
CourseRunSerializer
(
course_runs
,
many
=
True
,
context
=
self
.
get_serializer_context
()
)
.
data
def
test_retrieve
(
self
):
course_run
=
CourseFactory
()
url
=
reverse
(
'api:v1:course_run-detail'
,
kwargs
=
{
'pk'
:
str
(
course_run
.
id
)})
response
=
self
.
client
.
get
(
url
)
assert
response
.
status_code
==
200
assert
response
.
data
==
CourseRunSerializer
(
course_run
)
.
data
assert
response
.
data
==
CourseRunSerializer
(
course_run
,
context
=
self
.
get_serializer_context
()
)
.
data
def
test_retrieve_not_found
(
self
):
url
=
reverse
(
'api:v1:course_run-detail'
,
kwargs
=
{
'pk'
:
'course-v1:TestX+Test101x+1T2017'
})
...
...
@@ -104,7 +113,7 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
self
.
assert_access_role
(
course_run
,
user
,
role
)
course_run
=
modulestore
()
.
get_course
(
course_run
.
id
)
assert
response
.
data
==
CourseRunSerializer
(
course_run
)
.
data
assert
response
.
data
==
CourseRunSerializer
(
course_run
,
context
=
self
.
get_serializer_context
()
)
.
data
assert
course_run
.
display_name
==
title
self
.
assert_course_run_schedule
(
course_run
,
start
,
end
,
enrollment_start
,
enrollment_end
)
...
...
@@ -185,6 +194,39 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
self
.
assert_course_run_schedule
(
course_run
,
start
,
end
,
enrollment_start
,
enrollment_end
)
self
.
assert_access_role
(
course_run
,
user
,
role
)
def
test_images_upload
(
self
):
# http://www.django-rest-framework.org/api-guide/parsers/#fileuploadparser
course_run
=
CourseFactory
()
expected_filename
=
'course_image.png'
content_key
=
StaticContent
.
compute_location
(
course_run
.
id
,
expected_filename
)
assert
course_run
.
course_image
!=
expected_filename
try
:
contentstore
()
.
find
(
content_key
)
self
.
fail
(
'No image should be associated with a new course run.'
)
except
NotFoundError
:
pass
url
=
reverse
(
'api:v1:course_run-images'
,
kwargs
=
{
'pk'
:
str
(
course_run
.
id
)})
# PNG. Single black pixel
content
=
b
'
\x89
PNG
\r\n\x1a\n\x00\x00\x00\r
IHDR
\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90
wS'
\
b
'
\xde\x00\x00\x00\x0c
IDATx
\x9c
c```
\x00\x00\x00\x04\x00\x01\xf6\x17
8U
\x00\x00\x00\x00
IEND
\xae
B`
\x82
'
# We are intentionally passing the incorrect JPEG extension here
upload
=
SimpleUploadedFile
(
'card_image.jpg'
,
content
,
content_type
=
'image/png'
)
response
=
self
.
client
.
post
(
url
,
{
'card_image'
:
upload
},
format
=
'multipart'
)
assert
response
.
status_code
==
200
course_run
=
modulestore
()
.
get_course
(
course_run
.
id
)
assert
course_run
.
course_image
==
expected_filename
expected
=
{
'card_image'
:
RequestFactory
()
.
get
(
''
)
.
build_absolute_uri
(
course_image_url
(
course_run
))}
assert
response
.
data
==
expected
# There should now be an image stored
contentstore
()
.
find
(
content_key
)
def
test_rerun
(
self
):
course_run
=
ToyCourseFactory
()
start
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
.
replace
(
microsecond
=
0
)
...
...
cms/djangoapps/api/v1/views/course_runs.py
View file @
14b8c12e
...
...
@@ -2,13 +2,18 @@ from django.conf import settings
from
django.http
import
Http404
from
edx_rest_framework_extensions.authentication
import
JwtAuthentication
from
opaque_keys.edx.keys
import
CourseKey
from
rest_framework
import
permissions
,
status
,
viewsets
from
rest_framework
import
p
arsers
,
p
ermissions
,
status
,
viewsets
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework.decorators
import
detail_route
from
rest_framework.response
import
Response
from
contentstore.views.course
import
_accessible_courses_iter
,
get_course_and_check_access
from
..serializers.course_runs
import
CourseRunCreateSerializer
,
CourseRunRerunSerializer
,
CourseRunSerializer
from
..serializers.course_runs
import
(
CourseRunCreateSerializer
,
CourseRunImageSerializer
,
CourseRunRerunSerializer
,
CourseRunSerializer
)
class
CourseRunViewSet
(
viewsets
.
ViewSet
):
...
...
@@ -66,6 +71,19 @@ class CourseRunViewSet(viewsets.ViewSet):
serializer
.
save
()
return
Response
(
serializer
.
data
,
status
=
status
.
HTTP_201_CREATED
)
@detail_route
(
methods
=
[
'post'
,
'put'
],
parser_classes
=
(
parsers
.
FormParser
,
parsers
.
MultiPartParser
,),
serializer_class
=
CourseRunImageSerializer
)
def
images
(
self
,
request
,
pk
=
None
):
course_run_key
=
CourseKey
.
from_string
(
pk
)
user
=
request
.
user
course_run
=
self
.
get_course_run_or_raise_404
(
course_run_key
,
user
)
serializer
=
CourseRunImageSerializer
(
course_run
,
data
=
request
.
data
,
context
=
self
.
get_serializer_context
())
serializer
.
is_valid
(
raise_exception
=
True
)
serializer
.
save
()
return
Response
(
serializer
.
data
)
@detail_route
(
methods
=
[
'post'
])
def
rerun
(
self
,
request
,
pk
=
None
):
course_run_key
=
CourseKey
.
from_string
(
pk
)
...
...
cms/djangoapps/contentstore/views/assets.py
View file @
14b8c12e
...
...
@@ -9,25 +9,26 @@ from django.core.exceptions import PermissionDenied
from
django.http
import
HttpResponseBadRequest
,
HttpResponseNotFound
from
django.utils.translation
import
ugettext
as
_
from
django.views.decorators.csrf
import
ensure_csrf_cookie
from
django.views.decorators.http
import
require_
http_methods
,
require_POST
from
django.views.decorators.http
import
require_
POST
,
require_http_methods
from
opaque_keys.edx.keys
import
AssetKey
,
CourseKey
from
pymongo
import
ASCENDING
,
DESCENDING
from
xmodule.contentstore.content
import
StaticContent
from
xmodule.contentstore.django
import
contentstore
from
xmodule.exceptions
import
NotFoundError
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
contentstore.utils
import
reverse_course_url
from
contentstore.views.exception
import
AssetNotFoundException
from
contentstore.views.exception
import
AssetNotFoundException
,
AssetSizeTooLargeException
from
edxmako.shortcuts
import
render_to_response
from
openedx.core.djangoapps.contentserver.caching
import
del_cached_content
from
student.auth
import
has_course_author_access
from
util.date_utils
import
get_default_time_display
from
util.json_request
import
JsonResponse
from
xmodule.contentstore.content
import
StaticContent
from
xmodule.contentstore.django
import
contentstore
from
xmodule.exceptions
import
NotFoundError
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
__all__
=
[
'assets_handler'
]
# pylint: disable=unused-argument
...
...
@@ -204,51 +205,19 @@ def get_file_size(upload_file):
return
upload_file
.
size
@require_POST
@ensure_csrf_cookie
@login_required
def
_upload_asset
(
request
,
course_key
):
'''
This method allows for POST uploading of files into the course asset
library, which will be supported by GridFS in MongoDB.
'''
# Does the course actually exist?!? Get anything from it to prove its
# existence
try
:
modulestore
()
.
get_course
(
course_key
)
except
ItemNotFoundError
:
# no return it as a Bad Request response
logging
.
error
(
"Could not find course:
%
s"
,
course_key
)
return
HttpResponseBadRequest
()
# compute a 'filename' which is similar to the location formatting, we're
# using the 'filename' nomenclature since we're using a FileSystem paradigm
# here. We're just imposing the Location string formatting expectations to
# keep things a bit more consistent
upload_file
=
request
.
FILES
[
'file'
]
def
update_course_run_asset
(
course_key
,
upload_file
):
filename
=
upload_file
.
name
mime_type
=
upload_file
.
content_type
size
=
get_file_size
(
upload_file
)
# If file is greater than a specified size, reject the upload
# request and send a message to the user. Note that since
# the front-end may batch large file uploads in smaller chunks,
# we validate the file-size on the front-end in addition to
# validating on the backend. (see cms/static/js/views/assets.js)
max_file_size_in_bytes
=
settings
.
MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB
*
1000
**
2
max_size_in_mb
=
settings
.
MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB
max_file_size_in_bytes
=
max_size_in_mb
*
1000
**
2
if
size
>
max_file_size_in_bytes
:
return
JsonResponse
({
'error'
:
_
(
'File {filename} exceeds maximum size of '
'{size_mb} MB. Please follow the instructions here '
'to upload a file elsewhere and link to it instead: '
'{faq_url}'
)
.
format
(
filename
=
filename
,
size_mb
=
settings
.
MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB
,
faq_url
=
settings
.
MAX_ASSET_UPLOAD_FILE_SIZE_URL
,
)
},
status
=
413
)
msg
=
'File {filename} exceeds the maximum size of {max_size_in_mb} MB.'
.
format
(
filename
=
filename
,
max_size_in_mb
=
max_size_in_mb
)
raise
AssetSizeTooLargeException
(
msg
)
content_loc
=
StaticContent
.
compute_location
(
course_key
,
filename
)
...
...
@@ -261,15 +230,12 @@ def _upload_asset(request, course_key):
content
=
sc_partial
(
upload_file
.
read
())
tempfile_path
=
None
# first let's see if a thumbnail can be created
(
thumbnail_content
,
thumbnail_location
)
=
contentstore
()
.
generate_thumbnail
(
content
,
tempfile_path
=
tempfile_path
,
)
# Verify a thumbnail can be created
(
thumbnail_content
,
thumbnail_location
)
=
contentstore
()
.
generate_thumbnail
(
content
,
tempfile_path
=
tempfile_path
)
# delete cached thumbnail even if one couldn't be created this time (else
# the old thumbnail will continue to show)
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
del_cached_content
(
thumbnail_location
)
# now store thumbnail location only if we could create it
if
thumbnail_content
is
not
None
:
content
.
thumbnail_location
=
thumbnail_location
...
...
@@ -278,10 +244,41 @@ def _upload_asset(request, course_key):
contentstore
()
.
save
(
content
)
del_cached_content
(
content
.
location
)
return
content
@require_POST
@ensure_csrf_cookie
@login_required
def
_upload_asset
(
request
,
course_key
):
"""
This method allows for POST uploading of files into the course asset
library, which will be supported by GridFS in MongoDB.
"""
# Does the course actually exist?!? Get anything from it to prove its
# existence
try
:
modulestore
()
.
get_course
(
course_key
)
except
ItemNotFoundError
:
# no return it as a Bad Request response
logging
.
error
(
"Could not find course:
%
s"
,
course_key
)
return
HttpResponseBadRequest
()
# compute a 'filename' which is similar to the location formatting, we're
# using the 'filename' nomenclature since we're using a FileSystem paradigm
# here. We're just imposing the Location string formatting expectations to
# keep things a bit more consistent
upload_file
=
request
.
FILES
[
'file'
]
try
:
content
=
update_course_run_asset
(
course_key
,
upload_file
)
except
AssetSizeTooLargeException
as
ex
:
return
JsonResponse
({
'error'
:
ex
.
message
},
status
=
413
)
# readback the saved content - we need the database timestamp
readback
=
contentstore
()
.
find
(
content
.
location
)
locked
=
getattr
(
content
,
'locked'
,
False
)
re
sponse_payload
=
{
re
turn
JsonResponse
(
{
'asset'
:
_get_asset_json
(
content
.
name
,
content
.
content_type
,
...
...
@@ -291,9 +288,7 @@ def _upload_asset(request, course_key):
locked
),
'msg'
:
_
(
'Upload completed'
)
}
return
JsonResponse
(
response_payload
)
})
@require_http_methods
((
"DELETE"
,
"POST"
,
"PUT"
))
...
...
cms/djangoapps/contentstore/views/exception.py
View file @
14b8c12e
...
...
@@ -8,3 +8,10 @@ class AssetNotFoundException(Exception):
Raised when asset not found
"""
pass
class
AssetSizeTooLargeException
(
Exception
):
"""
Raised when the size of an uploaded asset exceeds the maximum size limit.
"""
pass
common/lib/xmodule/xmodule/modulestore/mixed.py
View file @
14b8c12e
...
...
@@ -636,12 +636,16 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
"""
# first make sure an existing course doesn't already exist in the mapping
course_key
=
self
.
make_course_key
(
org
,
course
,
run
)
log
.
info
(
'Creating course run
%
s...'
,
course_key
)
if
course_key
in
self
.
mappings
and
self
.
mappings
[
course_key
]
.
has_course
(
course_key
):
log
.
error
(
'Cannot create course run
%
s. It already exists!'
,
course_key
)
raise
DuplicateCourseError
(
course_key
,
course_key
)
# create the course
store
=
self
.
_verify_modulestore_support
(
None
,
'create_course'
)
course
=
store
.
create_course
(
org
,
course
,
run
,
user_id
,
**
kwargs
)
log
.
info
(
'Course run
%
s created successfully!'
,
course_key
)
# add new course to the mapping
self
.
mappings
[
course_key
]
=
store
...
...
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