264 lines
9.8 KiB
Python
264 lines
9.8 KiB
Python
import logging
|
|
import json
|
|
import os
|
|
import tarfile
|
|
import shutil
|
|
from tempfile import mkdtemp
|
|
from path import path
|
|
|
|
from django.conf import settings
|
|
from django.http import HttpResponse, HttpResponseBadRequest
|
|
from django.contrib.auth.decorators import login_required
|
|
from django_future.csrf import ensure_csrf_cookie
|
|
from django.core.urlresolvers import reverse
|
|
from django.core.servers.basehttp import FileWrapper
|
|
from django.core.files.temp import NamedTemporaryFile
|
|
|
|
from mitxmako.shortcuts import render_to_response
|
|
from cache_toolbox.core import del_cached_content
|
|
from auth.authz import create_all_course_groups
|
|
|
|
from xmodule.modulestore.xml_importer import import_from_xml
|
|
from xmodule.contentstore.django import contentstore
|
|
from xmodule.modulestore.xml_exporter import export_to_xml
|
|
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 ..utils import get_url_reverse
|
|
from .access import get_location_and_verify_access
|
|
|
|
|
|
__all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course']
|
|
|
|
|
|
@login_required
|
|
@ensure_csrf_cookie
|
|
def asset_index(request, org, course, name):
|
|
"""
|
|
Display an editable asset library
|
|
|
|
org, course, name: Attributes of the Location for the item to edit
|
|
"""
|
|
location = get_location_and_verify_access(request, org, course, name)
|
|
|
|
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
|
'org': org,
|
|
'course': course,
|
|
'coursename': name
|
|
})
|
|
|
|
course_module = modulestore().get_item(location)
|
|
|
|
course_reference = StaticContent.compute_location(org, course, name)
|
|
assets = contentstore().get_all_content_for_course(course_reference)
|
|
|
|
# sort in reverse upload date order
|
|
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
|
|
|
|
asset_display = []
|
|
for asset in assets:
|
|
asset_id = asset['_id']
|
|
display_info = {}
|
|
display_info['displayname'] = asset['displayname']
|
|
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
|
|
|
|
asset_location = StaticContent.compute_location(asset_id['org'], asset_id['course'], asset_id['name'])
|
|
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
|
|
|
|
# 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
|
|
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
|
|
|
|
asset_display.append(display_info)
|
|
|
|
return render_to_response('asset_index.html', {
|
|
'active_tab': 'assets',
|
|
'context_course': course_module,
|
|
'assets': asset_display,
|
|
'upload_asset_callback_url': upload_asset_callback_url
|
|
})
|
|
|
|
|
|
def upload_asset(request, org, course, coursename):
|
|
'''
|
|
cdodge: this method allows for POST uploading of files into the course asset library, which will
|
|
be supported by GridFS in MongoDB.
|
|
'''
|
|
if request.method != 'POST':
|
|
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
|
|
return HttpResponseBadRequest()
|
|
|
|
# construct a location from the passed in path
|
|
location = get_location_and_verify_access(request, org, course, coursename)
|
|
|
|
# Does the course actually exist?!? Get anything from it to prove its existance
|
|
|
|
try:
|
|
modulestore().get_item(location)
|
|
except:
|
|
# no return it as a Bad Request response
|
|
logging.error('Could not find course' + 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
|
|
|
|
filename = request.FILES['file'].name
|
|
mime_type = request.FILES['file'].content_type
|
|
filedata = request.FILES['file'].read()
|
|
|
|
content_loc = StaticContent.compute_location(org, course, filename)
|
|
content = StaticContent(content_loc, filename, mime_type, filedata)
|
|
|
|
# first let's see if a thumbnail can be created
|
|
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
|
|
|
|
# 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)
|
|
|
|
response_payload = {'displayname': content.name,
|
|
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
|
|
'url': StaticContent.get_url_path_from_location(content.location),
|
|
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
|
'msg': 'Upload completed'
|
|
}
|
|
|
|
response = HttpResponse(json.dumps(response_payload))
|
|
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
|
|
return response
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def import_course(request, org, course, name):
|
|
|
|
location = get_location_and_verify_access(request, org, course, name)
|
|
|
|
if request.method == 'POST':
|
|
filename = request.FILES['course-data'].name
|
|
|
|
if not filename.endswith('.tar.gz'):
|
|
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
|
|
|
|
data_root = path(settings.GITHUB_REPO_ROOT)
|
|
|
|
course_subdir = "{0}-{1}-{2}".format(org, course, name)
|
|
course_dir = data_root / course_subdir
|
|
if not course_dir.isdir():
|
|
os.mkdir(course_dir)
|
|
|
|
temp_filepath = course_dir / filename
|
|
|
|
logging.debug('importing course to {0}'.format(temp_filepath))
|
|
|
|
# stream out the uploaded files in chunks to disk
|
|
temp_file = open(temp_filepath, 'wb+')
|
|
for chunk in request.FILES['course-data'].chunks():
|
|
temp_file.write(chunk)
|
|
temp_file.close()
|
|
|
|
tar_file = tarfile.open(temp_filepath)
|
|
tar_file.extractall(course_dir + '/')
|
|
|
|
# find the 'course.xml' file
|
|
|
|
for dirpath, _dirnames, filenames in os.walk(course_dir):
|
|
for files in filenames:
|
|
if files == 'course.xml':
|
|
break
|
|
if files == 'course.xml':
|
|
break
|
|
|
|
if files != 'course.xml':
|
|
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
|
|
|
|
logging.debug('found course.xml at {0}'.format(dirpath))
|
|
|
|
if dirpath != course_dir:
|
|
for fname in os.listdir(dirpath):
|
|
shutil.move(dirpath / fname, course_dir)
|
|
|
|
_module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
|
|
[course_subdir], load_error_modules=False,
|
|
static_content_store=contentstore(),
|
|
target_location_namespace=Location(location),
|
|
draft_store=modulestore())
|
|
|
|
# we can blow this away when we're done importing.
|
|
shutil.rmtree(course_dir)
|
|
|
|
logging.debug('new course at {0}'.format(course_items[0].location))
|
|
|
|
create_all_course_groups(request.user, course_items[0].location)
|
|
|
|
return HttpResponse(json.dumps({'Status': 'OK'}))
|
|
else:
|
|
course_module = modulestore().get_item(location)
|
|
|
|
return render_to_response('import.html', {
|
|
'context_course': course_module,
|
|
'active_tab': 'import',
|
|
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
|
|
})
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def generate_export_course(request, org, course, name):
|
|
location = get_location_and_verify_access(request, org, course, name)
|
|
|
|
loc = Location(location)
|
|
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
|
|
|
|
root_dir = path(mkdtemp())
|
|
|
|
# export out to a tempdir
|
|
|
|
logging.debug('root = {0}'.format(root_dir))
|
|
|
|
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
|
|
#filename = root_dir / name + '.tar.gz'
|
|
|
|
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
|
tar_file = tarfile.open(name=export_file.name, mode='w:gz')
|
|
tar_file.add(root_dir / name, arcname=name)
|
|
tar_file.close()
|
|
|
|
# remove temp dir
|
|
shutil.rmtree(root_dir / name)
|
|
|
|
wrapper = FileWrapper(export_file)
|
|
response = HttpResponse(wrapper, content_type='application/x-tgz')
|
|
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
|
|
response['Content-Length'] = os.path.getsize(export_file.name)
|
|
return response
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def export_course(request, org, course, name):
|
|
|
|
location = get_location_and_verify_access(request, org, course, name)
|
|
|
|
course_module = modulestore().get_item(location)
|
|
|
|
return render_to_response('export.html', {
|
|
'context_course': course_module,
|
|
'active_tab': 'export',
|
|
'successful_import_redirect_url': ''
|
|
})
|