Commit f0108757 by Calen Pennington

Merge pull request #848 from MITx/feature/cas/lyla/asset-store-integration

Feature/cas/lyla/asset store integration
parents 6a7fb1d5 f06c67ef
......@@ -74,3 +74,8 @@ def compute_unit_state(unit):
return UnitState.private
else:
return UnitState.public
def get_date_display(date):
print date, type(date)
return date.strftime("%d %B, %Y at %I:%M %p")
import traceback
from util.json_request import expect_json
import exceptions
import json
......@@ -44,7 +45,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display
from xmodule.templates import all_templates
......@@ -618,7 +619,7 @@ def upload_asset(request, org, course, coursename):
if not has_access(request.user, location):
return HttpResponseForbidden()
# Does the course actually exist?!?
# Does the course actually exist?!? Get anything from it to prove its existance
try:
item = modulestore().get_item(location)
......@@ -635,23 +636,11 @@ def upload_asset(request, org, course, coursename):
mime_type = request.FILES['file'].content_type
filedata = request.FILES['file'].read()
file_location = StaticContent.compute_location(org, course, name)
content = StaticContent(file_location, name, mime_type, filedata)
# first commit to the DB
contentstore().save(content)
# then remove the cache so we're not serving up stale content
# NOTE: we're not re-populating the cache here as the DB owns the last-modified timestamp
# which is used when serving up static content. This integrity is needed for
# browser-side caching support. We *could* re-fetch the saved content so that we have the
# timestamp populated, but we might as well wait for the first real request to come in
# to re-populate the cache.
del_cached_content(content.location)
thumbnail_file_location = None
# if we're uploading an image, then let's generate a thumbnail so that we can
# serve it up when needed without having to rescale on the fly
# if the upload asset is an image, we can generate a thumbnail from it
# let's do so now, so that we have the thumbnail location which we need
# so that the asset can point to it
if mime_type.split('/')[0] == 'image':
try:
# not sure if this is necessary, but let's rewind the stream just in case
......@@ -673,24 +662,45 @@ def upload_asset(request, org, course, coursename):
thumbnail_file.seek(0)
# use a naming convention to associate originals with the thumbnail
thumbnail_name = content.generate_thumbnail_name()
thumbnail_name = StaticContent.generate_thumbnail_name(name)
# then just store this thumbnail as any other piece of content
thumbnail_file_location = StaticContent.compute_location(org, course,
thumbnail_name)
thumbnail_name, is_thumbnail=True)
thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name,
'image/jpeg', thumbnail_file)
contentstore().save(thumbnail_content)
# remove any cached content at this location, as thumbnails are treated just like any
# other bit of static content
del_cached_content(thumbnail_content.location)
# not sure if this is necessary, but let's rewind the stream just in case
request.FILES['file'].seek(0)
except:
# catch, log, and continue as thumbnails are not a hard requirement
logging.error('Failed to generate thumbnail for {0}. Continuing...'.format(name))
thumbnail_file_location = None
file_location = StaticContent.compute_location(org, course, name)
# create a StaticContent entity and point to the thumbnail
content = StaticContent(file_location, name, mime_type, filedata, thumbnail_location = thumbnail_file_location)
return HttpResponse('Upload completed')
# first commit to the DB
contentstore().save(content)
# then remove the cache so we're not serving up stale content
# NOTE: we're not re-populating the cache here as the DB owns the last-modified timestamp
# which is used when serving up static content. This integrity is needed for
# browser-side caching support. We *could* re-fetch the saved content so that we have the
# timestamp populated, but we might as well wait for the first real request to come in
# to re-populate the cache.
del_cached_content(content.location)
response = HttpResponse('Upload completed')
response['asset_url'] = StaticContent.get_url_path_from_location(file_location)
return response
'''
This view will return all CMS users who are editors for the specified course
......@@ -772,23 +782,69 @@ def remove_user(request, location):
return create_json_response()
@login_required
@ensure_csrf_cookie
def asset_index(request, location):
return render_to_response('asset_index.html',{})
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
def static_pages(request, org, course, coursename):
return render_to_response('static-pages.html', {})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})
def not_found(request):
return render_to_response('error.html', {'error': '404'})
def server_error(request):
return render_to_response('error.html', {'error': '500'})
\ No newline at end of file
return render_to_response('error.html', {'error': '500'})
@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 = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
upload_asset_callback_url = reverse('upload_asset', kwargs = {
'org' : org,
'course' : course,
'coursename' : name
})
course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference)
thumbnails = contentstore().get_all_content_thumbnails_for_course(course_reference)
asset_display = []
for asset in assets:
id = asset['_id']
display_info = {}
display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_date_display(asset['uploadDate'])
asset_location = StaticContent.compute_location(id['org'], id['course'], 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 = Location(asset.get('thumbnail_location', None))
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location)
asset_display.append(display_info)
return render_to_response('asset_index.html', {
'assets': asset_display,
'upload_asset_callback_url': upload_asset_callback_url
})
......@@ -23,6 +23,10 @@ $(document).ready(function() {
$modalCover.bind('click', hideHistoryModal);
$('.assets .upload-button').bind('click', showUploadModal);
$('.upload-modal .close-button').bind('click', hideModal);
$('a.show-xml').toggle(showEmbeddableXML, hideEmbeddableXML);
$('a.copy-button').toggle(showEmbeddableXML, hideEmbeddableXML);
$('.unit .item-actions .delete-button').bind('click', deleteUnit);
$('.new-unit-item').bind('click', createNewUnit);
$('.save-subsection').bind('click', saveSubsection);
......@@ -72,6 +76,17 @@ function removePolicyMetadata(e) {
_parent_el.remove();
else
_parent_el.appendTo("#policy-to-delete");
function showEmbeddableXML(e) {
$ceiling = $(this).parents('tr');
if ($ceiling.length === 0) $ceiling = $(this).parents('.upload-modal');
$ceiling.find('.embeddable-xml').html('<img src="'+$(this).attr('href')+'"/>');
}
function hideEmbeddableXML(e) {
$ceiling = $(this).parents('tr');
console.log($ceiling.length)
if ($ceiling.length === 0) $ceiling = $(this).parents('.upload-modal');
$ceiling.find('.embeddable-xml').html("");
}
......@@ -233,20 +248,40 @@ function showFileSelectionMenu(e) {
function startUpload(e) {
$('.upload-modal h1').html('Uploading…');
$('.upload-modal .file-name').html($('.file-input').val());
$('.upload-modal .file-chooser').ajaxSubmit({
beforeSend: resetUploadBar,
uploadProgress: showUploadFeedback,
complete: displayFinishedUpload
});
$('.upload-modal .choose-file-button').hide();
$('.upload-modal .progress-bar').removeClass('loaded').show();
$('.upload-modal .progress-fill').html('').css('width', '0').animate({
'width': '100%'
}, 1500);
setTimeout(markAsLoaded, 1500);
}
function resetUploadBar(){
var percentVal = '0%';
$('.upload-modal .progress-fill').width(percentVal)
$('.upload-modal .progress-fill').html(percentVal);
}
function showUploadFeedback(event, position, total, percentComplete) {
var percentVal = percentComplete + '%';
$('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal);
}
function displayFinishedUpload(xhr) {
if(xhr.status = 200){
markAsLoaded();
}
$('.upload-modal .copy-button').attr('href', xhr.getResponseHeader('asset_url'));
$('.upload-modal .progress-fill').html(xhr.responseText);
$('.upload-modal .choose-file-button').html('Load Another File').show();
}
function markAsLoaded() {
$('.upload-modal .copy-button').css('display', 'inline-block');
$('.upload-modal .progress-bar').addClass('loaded');
$('.upload-modal .progress-fill').html('loaded successfully');
$('.upload-modal .choose-file-button').html('Load Another File').show();
}
}
function hideModal(e) {
e.preventDefault();
......
......@@ -86,6 +86,9 @@
}
}
}
.show-xml {
@include blue-button;
}
}
.upload-modal {
......
......@@ -9,7 +9,8 @@
<div class="inner-wrapper">
<h1>Asset Library</h1>
<div class="page-actions">
<a href="#" class="upload-button">Upload New File</a><input type="text" class="asset-search-input search wip-box" placeholder="search assets" />
<a href="#" class="upload-button">Upload New File</a>
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" />
</div>
<article class="asset-library">
<table>
......@@ -22,149 +23,26 @@
</tr>
</thead>
<tbody>
% for asset in assets:
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/3185/screenshots/149571/picture_39.png"></div>
<div class="thumb"><img src="${asset['thumb_url']}"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-1.jpg</a>
<a href="${asset['url']}" class="filename">${asset['displayname']}</a>
<div class="embeddable-xml"></div>
</td>
<td class="date-col">
10/2/2012
${asset['uploadDate']}
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/4573/screenshots/157708/final.png"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-2.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/15101/screenshots/228625/star_wars_lightsbaer_2.jpg"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-2.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/3185/screenshots/149571/picture_39.png"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-1.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/4573/screenshots/157708/final.png"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-2.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/15101/screenshots/228625/star_wars_lightsbaer_2.jpg"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-2.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/3185/screenshots/149571/picture_39.png"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-1.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/4573/screenshots/157708/final.png"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-2.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/15101/screenshots/228625/star_wars_lightsbaer_2.jpg"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-2.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
</td>
</tr>
<tr>
<td class="thumb-col">
<div class="thumb"><img src="http://dribbble.s3.amazonaws.com/users/3185/screenshots/149571/picture_39.png"></div>
</td>
<td class="name-col">
<a href="#" class="filename">raygun-1.jpg</a>
</td>
<td class="date-col">
10/2/2012
</td>
<td class="embed-col">
<a href="#">copy</a>
<a class="show-xml" href="${asset['url']}">XML</a>
</td>
</tr>
% endfor
</tbody>
</table>
<nav class="pagination">
<nav class="pagination wip-box">
Page:
<ol class="pages">
<li>1</li>
......@@ -187,14 +65,17 @@
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<a href="#" class="copy-button">Copy Embed Link</a>
<div class="file-chooser">
<div class="embeddable-xml"></div>
<a href="#" class="copy-button">Show Embeddable XML</a>
<form class="file-chooser" action="${upload_asset_callback_url}"
method="post" enctype="multipart/form-data">
<a href="#" class="choose-file-button">Choose File</a>
<input type="file" class="file-input">
</div>
<input type="file" class="file-input" name="file">
</form>
</div>
</div>
<div class="modal-cover"></div>
</%block>
......@@ -33,6 +33,7 @@
<script src="${static.url('js/vendor/jquery.cookie.js')}"></script>
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
<script src="http://malsup.github.com/jquery.form.js"></script>
<script type="text/javascript">
document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
......
......@@ -30,16 +30,17 @@ urlpatterns = ('',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^assets/(?P<location>.*?)$', 'contentstore.views.asset_index', name='asset_index'),
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages', name='static_pages'),
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
# temporary landing page for a course
url(r'^landing/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'),
url(r'^not_found$', 'contentstore.views.not_found', name='not_found'),
url(r'^server_error$', 'contentstore.views.server_error', name='server_error')
url(r'^server_error$', 'contentstore.views.server_error', name='server_error'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
)
# User creation and updating views
......
XASSET_LOCATION_TAG = 'c4x'
XASSET_SRCREF_PREFIX = 'xasset:'
XASSET_THUMBNAIL_TAIL_NAME = '.thumbnail.jpg'
XASSET_THUMBNAIL_TAIL_NAME = '.jpg'
import os
import logging
from xmodule.modulestore import Location
class StaticContent(object):
def __init__(self, loc, name, content_type, data, last_modified_at=None):
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None):
self.location = loc
self.name = name #a display string which can be edited, and thus not part of the location which needs to be fixed
self.content_type = content_type
self.data = data
self.last_modified_at = last_modified_at
self.thumbnail_location = thumbnail_location
@property
def is_thumbnail(self):
return self.name.endswith(XASSET_THUMBNAIL_TAIL_NAME)
return self.location.category == 'thumbnail'
def generate_thumbnail_name(self):
return ('{0}'+XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(self.name)[0])
@staticmethod
def generate_thumbnail_name(original_name):
return ('{0}'+XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0])
@staticmethod
def compute_location(org, course, name, revision=None):
return Location([XASSET_LOCATION_TAG, org, course, 'asset', name, revision])
def compute_location(org, course, name, revision=None, is_thumbnail=False):
return Location([XASSET_LOCATION_TAG, org, course, 'asset' if not is_thumbnail else 'thumbnail', Location.clean(name), revision])
def get_id(self):
return StaticContent.get_id_from_location(self.location)
......@@ -34,7 +36,10 @@ class StaticContent(object):
@staticmethod
def get_url_path_from_location(location):
return "/{tag}/{org}/{course}/{category}/{name}".format(**location.dict())
if location is not None:
return "/{tag}/{org}/{course}/{category}/{name}".format(**location.dict())
else:
return None
@staticmethod
def get_id_from_location(location):
......@@ -65,4 +70,23 @@ class ContentStore(object):
raise NotImplementedError
def get_all_content_for_course(self, location):
'''
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
[
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
u'org': u'MITx', u'revision': None}, u'md5': u'ff1532598830e3feac91c2449eaa60d6'},
....
]
'''
raise NotImplementedError
......@@ -28,7 +28,9 @@ class MongoContentStore(ContentStore):
if self.fs.exists({"_id" : id}):
self.fs.delete(id)
with self.fs.new_file(_id = id, filename=content.get_url_path(), content_type=content.content_type, displayname=content.name) as fp:
with self.fs.new_file(_id = id, filename=content.get_url_path(), content_type=content.content_type,
displayname=content.name, thumbnail_location=content.thumbnail_location) as fp:
fp.write(content.data)
return content
......@@ -38,11 +40,18 @@ class MongoContentStore(ContentStore):
id = StaticContent.get_id_from_location(location)
try:
with self.fs.get(id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(), fp.uploadDate)
return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
fp.uploadDate, thumbnail_location = fp.thumbnail_location if 'thumbnail_location' in fp else None)
except NoFile:
raise NotFoundError()
def get_all_content_info_for_course(self, location):
def get_all_content_thumbnails_for_course(self, location):
return self._get_all_content_for_course(location, get_thumbnails = True)
def get_all_content_for_course(self, location):
return self._get_all_content_for_course(location, get_thumbnails = False)
def _get_all_content_for_course(self, location, get_thumbnails = False):
'''
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
......@@ -62,7 +71,8 @@ class MongoContentStore(ContentStore):
]
'''
course_filter = Location(XASSET_LOCATION_TAG, category="asset",course=location.course,org=location.org)
course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail",
course=location.course,org=location.org)
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
items = self.fs_files.find(location_to_query(course_filter))
return list(items)
......
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