257 lines
9.9 KiB
Python
257 lines
9.9 KiB
Python
import logging
|
|
from functools import partial
|
|
|
|
from django.http import HttpResponseBadRequest
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.views.decorators.http import require_http_methods
|
|
from django_future.csrf import ensure_csrf_cookie
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from mitxmako.shortcuts import render_to_response
|
|
from cache_toolbox.core import del_cached_content
|
|
|
|
from xmodule.contentstore.django import contentstore
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.modulestore import Location
|
|
from xmodule.contentstore.content import StaticContent
|
|
from xmodule.util.date_utils import get_default_time_display
|
|
from xmodule.modulestore import InvalidLocationError
|
|
from xmodule.exceptions import NotFoundError
|
|
from django.core.exceptions import PermissionDenied
|
|
from xmodule.modulestore.django import loc_mapper
|
|
from .access import has_access
|
|
from xmodule.modulestore.locator import BlockUsageLocator
|
|
|
|
from util.json_request import JsonResponse
|
|
from django.http import HttpResponseNotFound
|
|
import json
|
|
from django.utils.translation import ugettext as _
|
|
from pymongo import DESCENDING
|
|
|
|
|
|
__all__ = ['assets_handler']
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def assets_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None, asset_id=None):
|
|
"""
|
|
The restful handler for assets.
|
|
It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
|
|
deleting assets, and changing the "locked" state of an asset.
|
|
|
|
GET
|
|
html: return html page of all course assets (note though that a range of assets can be requested using start
|
|
and max query parameters)
|
|
json: not currently supported
|
|
POST
|
|
json: create (or update?) an asset. The only updating that can be done is changing the lock state.
|
|
PUT
|
|
json: update the locked state of an asset
|
|
DELETE
|
|
json: delete an asset
|
|
"""
|
|
location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
|
|
if not has_access(request.user, location):
|
|
raise PermissionDenied()
|
|
|
|
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
|
if request.method == 'GET':
|
|
raise NotImplementedError('coming soon')
|
|
else:
|
|
return _update_asset(request, location, asset_id)
|
|
elif request.method == 'GET': # assume html
|
|
return _asset_index(request, location)
|
|
else:
|
|
return HttpResponseNotFound()
|
|
|
|
|
|
def _asset_index(request, location):
|
|
"""
|
|
Display an editable asset library.
|
|
|
|
Supports start (0-based index into the list of assets) and max query parameters.
|
|
"""
|
|
old_location = loc_mapper().translate_locator_to_location(location)
|
|
|
|
course_module = modulestore().get_item(old_location)
|
|
maxresults = request.REQUEST.get('max', None)
|
|
start = request.REQUEST.get('start', None)
|
|
course_reference = StaticContent.compute_location(old_location.org, old_location.course, old_location.name)
|
|
if maxresults is not None:
|
|
maxresults = int(maxresults)
|
|
start = int(start) if start else 0
|
|
assets = contentstore().get_all_content_for_course(
|
|
course_reference, start=start, maxresults=maxresults,
|
|
sort=[('uploadDate', DESCENDING)]
|
|
)
|
|
else:
|
|
assets = contentstore().get_all_content_for_course(
|
|
course_reference, sort=[('uploadDate', DESCENDING)]
|
|
)
|
|
|
|
asset_json = []
|
|
for asset in assets:
|
|
asset_id = asset['_id']
|
|
asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name'])
|
|
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
|
|
_thumbnail_location = asset.get('thumbnail_location', None)
|
|
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
|
|
|
|
asset_locked = asset.get('locked', False)
|
|
asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location, asset_locked))
|
|
|
|
return render_to_response('asset_index.html', {
|
|
'context_course': course_module,
|
|
'asset_list': json.dumps(asset_json),
|
|
'asset_callback_url': location.url_reverse('assets/', '')
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def _upload_asset(request, location):
|
|
'''
|
|
This method allows for POST uploading of files into the course asset
|
|
library, which will be supported by GridFS in MongoDB.
|
|
'''
|
|
old_location = loc_mapper().translate_locator_to_location(location)
|
|
|
|
# Does the course actually exist?!? Get anything from it to prove its
|
|
# existence
|
|
try:
|
|
modulestore().get_item(old_location)
|
|
except:
|
|
# no return it as a Bad Request response
|
|
logging.error('Could not find course' + old_location)
|
|
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']
|
|
filename = upload_file.name
|
|
mime_type = upload_file.content_type
|
|
|
|
content_loc = StaticContent.compute_location(old_location.org, old_location.course, filename)
|
|
|
|
chunked = upload_file.multiple_chunks()
|
|
sc_partial = partial(StaticContent, content_loc, filename, mime_type)
|
|
if chunked:
|
|
content = sc_partial(upload_file.chunks())
|
|
tempfile_path = upload_file.temporary_file_path()
|
|
else:
|
|
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
|
|
)
|
|
|
|
# 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
|
|
|
|
# then commit the content
|
|
contentstore().save(content)
|
|
del_cached_content(content.location)
|
|
|
|
# readback the saved content - we need the database timestamp
|
|
readback = contentstore().find(content.location)
|
|
|
|
locked = getattr(content, 'locked', False)
|
|
response_payload = {
|
|
'asset': _get_asset_json(content.name, readback.last_modified_at, content.location, content.thumbnail_location, locked),
|
|
'msg': _('Upload completed')
|
|
}
|
|
|
|
return JsonResponse(response_payload)
|
|
|
|
|
|
@require_http_methods(("DELETE", "POST", "PUT"))
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def _update_asset(request, location, asset_id):
|
|
"""
|
|
restful CRUD operations for a course asset.
|
|
Currently only DELETE, POST, and PUT methods are implemented.
|
|
|
|
asset_id: the URL of the asset (used by Backbone as the id)
|
|
"""
|
|
def get_asset_location(asset_id):
|
|
""" Helper method to get the location (and verify it is valid). """
|
|
try:
|
|
return StaticContent.get_location_from_path(asset_id)
|
|
except InvalidLocationError as err:
|
|
# return a 'Bad Request' to browser as we have a malformed Location
|
|
return JsonResponse({"error": err.message}, status=400)
|
|
|
|
if request.method == 'DELETE':
|
|
loc = get_asset_location(asset_id)
|
|
# Make sure the item to delete actually exists.
|
|
try:
|
|
content = contentstore().find(loc)
|
|
except NotFoundError:
|
|
return JsonResponse(status=404)
|
|
|
|
# ok, save the content into the trashcan
|
|
contentstore('trashcan').save(content)
|
|
|
|
# see if there is a thumbnail as well, if so move that as well
|
|
if content.thumbnail_location is not None:
|
|
try:
|
|
thumbnail_content = contentstore().find(content.thumbnail_location)
|
|
contentstore('trashcan').save(thumbnail_content)
|
|
# hard delete thumbnail from origin
|
|
contentstore().delete(thumbnail_content.get_id())
|
|
# remove from any caching
|
|
del_cached_content(thumbnail_content.location)
|
|
except:
|
|
logging.warning('Could not delete thumbnail: ' + content.thumbnail_location)
|
|
|
|
# delete the original
|
|
contentstore().delete(content.get_id())
|
|
# remove from cache
|
|
del_cached_content(content.location)
|
|
return JsonResponse()
|
|
|
|
elif request.method in ('PUT', 'POST'):
|
|
if 'file' in request.FILES:
|
|
return _upload_asset(request, location)
|
|
else:
|
|
# Update existing asset
|
|
try:
|
|
modified_asset = json.loads(request.body)
|
|
except ValueError:
|
|
return HttpResponseBadRequest()
|
|
asset_id = modified_asset['url']
|
|
asset_location = get_asset_location(asset_id)
|
|
contentstore().set_attr(asset_location, 'locked', modified_asset['locked'])
|
|
# Delete the asset from the cache so we check the lock status the next time it is requested.
|
|
del_cached_content(asset_location)
|
|
return JsonResponse(modified_asset, status=201)
|
|
|
|
|
|
def _get_asset_json(display_name, date, location, thumbnail_location, locked):
|
|
"""
|
|
Helper method for formatting the asset information to send to client.
|
|
"""
|
|
asset_url = StaticContent.get_url_path_from_location(location)
|
|
return {
|
|
'display_name': display_name,
|
|
'date_added': get_default_time_display(date),
|
|
'url': asset_url,
|
|
'portable_url': StaticContent.get_static_path_from_location(location),
|
|
'thumbnail': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None,
|
|
'locked': locked,
|
|
# Needed for Backbone delete/update.
|
|
'id': asset_url
|
|
}
|