Commit 01d7d48f by cahrens

Create backbone models/views for assets.

cleanup

Updates.

Testing changes.

Testing changes.

asset-index progress state cleanjup.

cleanup
parent 97b738bc
......@@ -2,7 +2,10 @@
Unit tests for the asset upload endpoint.
"""
import json
#pylint: disable=C0111
#pylint: disable=W0621
#pylint: disable=W0212
from datetime import datetime
from io import BytesIO
from pytz import UTC
......@@ -12,7 +15,9 @@ from django.core.urlresolvers import reverse
from contentstore.views import assets
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
class AssetsTestCase(CourseTestCase):
def setUp(self):
......@@ -27,22 +32,27 @@ class AssetsTestCase(CourseTestCase):
resp = self.client.get(self.url)
self.assertEquals(resp.status_code, 200)
def test_json(self):
resp = self.client.get(
self.url,
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEquals(resp.status_code, 200)
content = json.loads(resp.content)
self.assertIsInstance(content, list)
def test_static_url_generation(self):
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
path = StaticContent.get_static_path_from_location(location)
self.assertEquals(path, '/static/my_file_name.jpg')
class AssetsToyCourseTestCase(CourseTestCase):
"""
Tests the assets returned from asset_index for the toy test course.
"""
def test_toy_assets(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=contentstore(), verbose=True)
url = reverse("asset_index", kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
resp = self.client.get(url)
# Test a small portion of the asset data passed to the client.
self.assertContains(resp, "new CMS.Models.AssetCollection([{")
self.assertContains(resp, "/c4x/edX/toy/asset/handouts_sample_handout.txt")
class UploadTestCase(CourseTestCase):
"""
Unit tests for uploading a file
......@@ -71,32 +81,25 @@ class UploadTestCase(CourseTestCase):
self.assertEquals(resp.status_code, 405)
class AssetsToJsonTestCase(TestCase):
class AssetToJsonTestCase(TestCase):
"""
Unit tests for transforming the results of a database call into something
Unit test for transforming asset information into something
we can send out to the client via JSON.
"""
def test_basic(self):
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
asset = {
"displayname": "foo",
"chunkSize": 512,
"filename": "foo.png",
"length": 100,
"uploadDate": upload_date,
"_id": {
"course": "course",
"org": "org",
"revision": 12,
"category": "category",
"name": "name",
"tag": "tag",
}
}
output = assets.assets_to_json_dict([asset])
self.assertEquals(len(output), 1)
compare = output[0]
self.assertEquals(compare["name"], "foo")
self.assertEquals(compare["path"], "foo.png")
self.assertEquals(compare["uploaded"], upload_date.isoformat())
self.assertEquals(compare["id"], "/tag/org/course/12/category/name")
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
thumbnail_location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name_thumb.jpg'])
output = assets._get_asset_json("my_file", upload_date, location, thumbnail_location)
self.assertEquals(output["display_name"], "my_file")
self.assertEquals(output["date_added"], "Jun 01, 2013 at 10:30 UTC")
self.assertEquals(output["url"], "/i4x/foo/bar/asset/my_file_name.jpg")
self.assertEquals(output["portable_url"], "/static/my_file_name.jpg")
self.assertEquals(output["thumbnail"], "/i4x/foo/bar/asset/my_file_name_thumb.jpg")
self.assertEquals(output["id"], output["url"])
output = assets._get_asset_json("name", upload_date, location, None)
self.assertIsNone(output["thumbnail"])
......@@ -593,9 +593,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'})
self.assertEqual(resp.status_code, 200)
url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall', 'asset_id': '/c4x/edX/toy/asset/sample_static.txt'})
resp = self.client.delete(url)
self.assertEqual(resp.status_code, 204)
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
......@@ -628,7 +628,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_empty_trashcan(self):
'''
This test will exercise the empting of the asset trashcan
This test will exercise the emptying of the asset trashcan
'''
content_store = contentstore()
trash_store = contentstore('trashcan')
......@@ -644,9 +644,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view
url = reverse('remove_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'})
self.assertEqual(resp.status_code, 200)
url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall', 'asset_id': '/c4x/edX/toy/asset/sample_static.txt'})
resp = self.client.delete(url)
self.assertEqual(resp.status_code, 204)
# make sure there's something in the trashcan
all_assets = trash_store.get_all_content_for_course(course_location)
......
import logging
import json
import os
import tarfile
import shutil
import cgi
import re
from functools import partial
from tempfile import mkdtemp
from path import path
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
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.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from django.views.decorators.http import require_POST, require_http_methods
from django.views.decorators.http import require_POST
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 xmodule.modulestore import InvalidLocationError
from xmodule.exceptions import NotFoundError, SerializationError
from xmodule.exceptions import NotFoundError
from .access import get_location_and_verify_access
from util.json_request import JsonResponse
import json
from django.utils.translation import ugettext as _
__all__ = ['asset_index', 'upload_asset']
def assets_to_json_dict(assets):
"""
Transform the results of a contentstore query into something appropriate
for output via JSON.
"""
ret = []
for asset in assets:
obj = {
"name": asset.get("displayname", ""),
"chunkSize": asset.get("chunkSize", 0),
"path": asset.get("filename", ""),
"length": asset.get("length", 0),
}
uploaded = asset.get("uploadDate")
if uploaded:
obj["uploaded"] = uploaded.isoformat()
thumbnail = asset.get("thumbnail_location")
if thumbnail:
obj["thumbnail"] = thumbnail
id_info = asset.get("_id")
if id_info:
obj["id"] = "/{tag}/{org}/{course}/{revision}/{category}/{name}" \
.format(
org=id_info.get("org", ""),
course=id_info.get("course", ""),
revision=id_info.get("revision", ""),
tag=id_info.get("tag", ""),
category=id_info.get("category", ""),
name=id_info.get("name", ""),
)
ret.append(obj)
return ret
@login_required
@ensure_csrf_cookie
......@@ -96,32 +53,21 @@ def asset_index(request, org, course, name):
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
if request.META.get('HTTP_ACCEPT', "").startswith("application/json"):
return JsonResponse(assets_to_json_dict(assets))
asset_display = []
asset_json = []
for asset in assets:
asset_id = asset['_id']
display_info = {}
display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'])
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)
display_info['portable_url'] = StaticContent.get_static_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)
asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location))
return render_to_response('asset_index.html', {
'context_course': course_module,
'assets': asset_display,
'asset_list': json.dumps(asset_json),
'upload_asset_callback_url': upload_asset_callback_url,
'remove_asset_callback_url': reverse('remove_asset', kwargs={
'update_asset_callback_url': reverse('update_asset', kwargs={
'org': org,
'course': course,
'name': name
......@@ -171,9 +117,6 @@ def upload_asset(request, org, course, coursename):
content = sc_partial(upload_file.read())
tempfile_path = None
thumbnail_content = None
thumbnail_location = None
# first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
content,
......@@ -195,67 +138,73 @@ def upload_asset(request, org, course, coursename):
readback = contentstore().find(content.location)
response_payload = {
'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at),
'url': StaticContent.get_url_path_from_location(content.location),
'portable_url': StaticContent.get_static_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'
'asset': _get_asset_json(content.name, readback.last_modified_at, content.location, content.thumbnail_location),
'msg': _('Upload completed')
}
response = JsonResponse(response_payload)
return response
return JsonResponse(response_payload)
@ensure_csrf_cookie
@require_http_methods("DELETE")
@login_required
def remove_asset(request, org, course, name):
'''
This method will perform a 'soft-delete' of an asset, which is basically to
copy the asset from the main GridFS collection and into a Trashcan
'''
get_location_and_verify_access(request, org, course, name)
location = request.POST['location']
# make sure the location is valid
try:
loc = StaticContent.get_location_from_path(location)
except InvalidLocationError:
# return a 'Bad Request' to browser as we have a malformed Location
response = HttpResponse()
response.status_code = 400
return response
# also make sure the item to delete actually exists
try:
content = contentstore().find(loc)
except NotFoundError:
response = HttpResponse()
response.status_code = 404
return response
@ensure_csrf_cookie
def update_asset(request, org, course, name, asset_id):
"""
restful CRUD operations for a course asset.
Currently only the DELETE method is implemented.
# ok, save the content into the trashcan
contentstore('trashcan').save(content)
org, course, name: Attributes of the Location for the item to edit
asset_id: the URL of the asset (used by Backbone as the id)
"""
get_location_and_verify_access(request, org, course, name)
# see if there is a thumbnail as well, if so move that as well
if content.thumbnail_location is not None:
if request.method == 'DELETE':
# make sure the location is valid
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:
pass # OK if this is left dangling
# delete the original
contentstore().delete(content.get_id())
# remove from cache
del_cached_content(content.location)
return HttpResponse()
loc = 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)
# also 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:
pass # OK if this is left dangling
# delete the original
contentstore().delete(content.get_id())
# remove from cache
del_cached_content(content.location)
return JsonResponse()
def _get_asset_json(display_name, date, location, thumbnail_location):
"""
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,
# Needed for Backbone delete/update.
'id': asset_url
}
......@@ -254,8 +254,10 @@ PIPELINE_JS = {
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/models/uploads.js', 'js/views/uploads.js',
'js/models/textbook.js', 'js/views/textbook.js',
'js/views/assets.js', 'js/src/utility.js',
'js/models/settings/course_grading_policy.js'],
'js/src/utility.js',
'js/models/settings/course_grading_policy.js',
'js/models/asset.js', 'js/models/assets.js',
'js/views/assets_view.js', 'js/views/asset_view.js'],
'output_filename': 'js/cms-application.js',
'test_order': 0
},
......
describe "CMS.Models.Asset", ->
beforeEach ->
CMS.URL.UPDATE_ASSET = "/update_asset/"
@model = new CMS.Models.Asset({id: "/c4x/id"})
afterEach ->
delete CMS.URL.UPDATE_ASSET
it "should have a url set", ->
expect(@model.url()).toEqual("/update_asset//c4x/id")
feedbackTpl = readFixtures('system-feedback.underscore')
assetTpl = readFixtures('asset.underscore')
describe "CMS.Views.Asset", ->
beforeEach ->
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
appendSetFixtures(sandbox({id: "page-prompt"}))
@model = new CMS.Models.Asset({display_name: "test asset", url: 'actual_asset_url', portable_url: 'portable_url', date_added: 'date', thumbnail: null, id: 'id'})
spyOn(@model, "destroy").andCallThrough()
@collection = new CMS.Models.AssetCollection([@model])
@view = new CMS.Views.Asset({model: @model})
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
@promptSpies.show.andReturn(@promptSpies)
describe "Basic", ->
it "should render properly", ->
@view.render()
expect(@view.$el).toContainText("test asset")
it "should pop a delete confirmation when the delete button is clicked", ->
@view.render().$(".remove-asset-button").click()
expect(@promptSpies.constructor).toHaveBeenCalled()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
expect(ctorOptions.title).toMatch('Delete File Confirmation')
# hasn't actually been removed
expect(@model.destroy).not.toHaveBeenCalled()
expect(@collection).toContain(@model)
describe "AJAX", ->
beforeEach ->
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Confirmation", ["show"])
@savingSpies.show.andReturn(@savingSpies)
afterEach ->
@xhr.restore()
it "should destroy itself on confirmation", ->
@view.render().$(".remove-asset-button").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
# run the primary function to indicate confirmation
ctorOptions.actions.primary.click(@promptSpies)
# AJAX request has been sent, but not yet returned
expect(@model.destroy).toHaveBeenCalled()
expect(@requests.length).toEqual(1)
expect(@savingSpies.constructor).not.toHaveBeenCalled()
expect(@collection.contains(@model)).toBeTruthy()
# return a success response
@requests[0].respond(200)
expect(@savingSpies.constructor).toHaveBeenCalled()
expect(@savingSpies.show).toHaveBeenCalled()
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
expect(savingOptions.title).toMatch("Your file has been deleted.")
expect(@collection.contains(@model)).toBeFalsy()
it "should not destroy itself if server errors", ->
@view.render().$(".remove-asset-button").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
# run the primary function to indicate confirmation
ctorOptions.actions.primary.click(@promptSpies)
# AJAX request has been sent, but not yet returned
expect(@model.destroy).toHaveBeenCalled()
# return an error response
@requests[0].respond(404)
expect(@savingSpies.constructor).not.toHaveBeenCalled()
expect(@collection.contains(@model)).toBeTruthy()
describe "CMS.Views.Assets", ->
beforeEach ->
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
appendSetFixtures(sandbox({id: "asset_table_body"}))
@collection = new CMS.Models.AssetCollection(
[
{display_name: "test asset 1", url: 'actual_asset_url_1', portable_url: 'portable_url_1', date_added: 'date_1', thumbnail: null, id: 'id_1'},
{display_name: "test asset 2", url: 'actual_asset_url_2', portable_url: 'portable_url_2', date_added: 'date_2', thumbnail: null, id: 'id_2'}
])
@view = new CMS.Views.Assets({collection: @collection, el: $('#asset_table_body')})
@promptSpies = spyOnConstructor(CMS.Views.Prompt, "Warning", ["show", "hide"])
@promptSpies.show.andReturn(@promptSpies)
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
delete window.analytics
delete window.course_location_analytics
describe "Basic", ->
it "should render both assets", ->
@view.render()
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).toContainText("test asset 2")
it "should remove the deleted asset from the view", ->
# Delete the 2nd asset with success from server.
@view.render().$(".remove-asset-button")[1].click()
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
@requests[0].respond(200)
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).not.toContainText("test asset 2")
it "does not remove asset if deletion failed", ->
# Delete the 2nd asset, but mimic a failure from the server.
@view.render().$(".remove-asset-button")[1].click()
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
@requests[0].respond(404)
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).toContainText("test asset 2")
it "adds an asset if asset does not already exist", ->
@view.render()
model = new CMS.Models.Asset({display_name: "new asset", url: 'new_actual_asset_url', portable_url: 'portable_url', date_added: 'date', thumbnail: null, id: 'idx'})
@view.addAsset(model)
expect(@view.$el).toContainText("new asset")
expect(@collection.models.indexOf(model)).toBe(0)
expect(@collection.models.length).toBe(3)
it "does not add an asset if asset already exists", ->
@view.render()
spyOn(@collection, "add").andCallThrough()
model = @collection.models[1]
@view.addAsset(model)
expect(@collection.add).not.toHaveBeenCalled()
......@@ -11,15 +11,11 @@ describe "CMS.Views.UploadDialog", ->
@model = new CMS.Models.FileUpload(
mimeTypes: ['application/pdf']
)
@chapter = new CMS.Models.Chapter()
@dialogResponse = dialogResponse = []
@view = new CMS.Views.UploadDialog(
model: @model,
onSuccess: (response) =>
options = {}
if !@chapter.get('name')
options.name = response.displayname
options.asset_path = response.url
@chapter.set(options)
dialogResponse.push(response.response)
)
spyOn(@view, 'remove').andCallThrough()
......@@ -66,7 +62,6 @@ describe "CMS.Views.UploadDialog", ->
expect(@view.$el).toContain("#upload_error")
expect(@view.$(".action-upload")).toHaveClass("disabled")
it "adds body class on show()", ->
@view.show()
expect(@view.options.shown).toBeTruthy()
......@@ -99,11 +94,10 @@ describe "CMS.Views.UploadDialog", ->
expect(request.method).toEqual("POST")
request.respond(200, {"Content-Type": "application/json"},
'{"displayname": "starfish", "url": "/uploaded/starfish.pdf"}')
'{"response": "dummy_response"}')
expect(@model.get("uploading")).toBeFalsy()
expect(@model.get("finished")).toBeTruthy()
expect(@chapter.get("name")).toEqual("starfish")
expect(@chapter.get("asset_path")).toEqual("/uploaded/starfish.pdf")
expect(@dialogResponse.pop()).toEqual("dummy_response")
it "can handle upload errors", ->
@view.upload()
......@@ -114,7 +108,7 @@ describe "CMS.Views.UploadDialog", ->
it "removes itself after two seconds on successful upload", ->
@view.upload()
@requests[0].respond(200, {"Content-Type": "application/json"},
'{"displayname": "starfish", "url": "/uploaded/starfish.pdf"}')
'{"response": "dummy_response"}')
expect(@view.remove).not.toHaveBeenCalled()
@clock.tick(2001)
expect(@view.remove).toHaveBeenCalled()
......@@ -421,11 +421,6 @@ function _deleteItem($el, type) {
confirm.show();
}
function markAsLoaded() {
$('.upload-modal .copy-button').css('display', 'inline-block');
$('.upload-modal .progress-bar').addClass('loaded');
}
function hideModal(e) {
if (e) {
e.preventDefault();
......
/**
* Simple model for an asset.
*/
CMS.Models.Asset = Backbone.Model.extend({
defaults: {
display_name: "",
thumbnail: "",
date_added: "",
url: "",
portable_url: "",
is_locked: false
},
url: function() {
return CMS.URL.UPDATE_ASSET + this.id;
}
});
CMS.Models.AssetCollection = Backbone.Collection.extend({
model : CMS.Models.Asset
});
CMS.Views.Asset = Backbone.View.extend({
initialize: function() {
this.template = _.template($("#asset-tpl").text());
},
tagName: "tr",
events: {
"click .remove-asset-button": "confirmDelete"
},
render: function() {
this.$el.html(this.template({
display_name: this.model.get('display_name'),
thumbnail: this.model.get('thumbnail'),
date_added: this.model.get('date_added'),
url: this.model.get('url'),
portable_url: this.model.get('portable_url')}));
return this;
},
confirmDelete: function(e) {
if(e && e.preventDefault) { e.preventDefault(); }
var asset = this.model, collection = this.model.collection;
new CMS.Views.Prompt.Warning({
title: gettext("Delete File Confirmation"),
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
actions: {
primary: {
text: gettext("Delete"),
click: function (view) {
view.hide();
asset.destroy({
wait: true, // Don't remove the asset from the collection until successful.
success: function () {
new CMS.Views.Notification.Confirmation({
title: gettext("Your file has been deleted."),
closeIcon: false,
maxShown: 2000
}).show()
}
}
);
}
},
secondary: [
{
text: gettext("Cancel"),
click: function (view) {
view.hide();
}
}
]
}
}).show();
}
});
$(document).ready(function() {
$('.uploads .upload-button').bind('click', showUploadModal);
$('.upload-modal .close-button').bind('click', hideModal);
$('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu);
$('.remove-asset-button').bind('click', removeAsset);
});
function removeAsset(e){
e.preventDefault();
var that = this;
var msg = new CMS.Views.Prompt.Warning({
title: gettext("Delete File Confirmation"),
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
actions: {
primary: {
text: gettext("OK"),
click: function(view) {
// call the back-end to actually remove the asset
var url = $('.asset-library').data('remove-asset-callback-url');
var row = $(that).closest('tr');
$.post(url,
{ 'location': row.data('id') },
function() {
// show the post-commit confirmation
var deleted = new CMS.Views.Notification.Confirmation({
title: gettext("Your file has been deleted."),
closeIcon: false,
maxShown: 2000
});
deleted.show();
row.remove();
analytics.track('Deleted Asset', {
'course': course_location_analytics,
'id': row.data('id')
});
}
);
view.hide();
}
},
secondary: [{
text: gettext("Cancel"),
click: function(view) {
view.hide();
}
}]
}
});
return msg.show();
}
function showUploadModal(e) {
e.preventDefault();
resetUploadModal();
$modal = $('.upload-modal').show();
$('.upload-modal .file-chooser').fileupload({
dataType: 'json',
type: 'POST',
maxChunkSize: 100 * 1000 * 1000, // 100 MB
autoUpload: true,
progressall: function(e, data) {
var percentComplete = parseInt((100 * data.loaded) / data.total, 10);
showUploadFeedback(e, percentComplete);
},
maxFileSize: 100 * 1000 * 1000, // 100 MB
maxNumberofFiles: 100,
add: function(e, data) {
data.process().done(function () {
data.submit();
});
},
done: function(e, data) {
displayFinishedUpload(data.result);
}
});
$('.file-input').bind('change', startUpload);
$modalCover.show();
}
function showFileSelectionMenu(e) {
e.preventDefault();
$('.file-input').click();
}
function startUpload(e) {
var files = $('.file-input').get(0).files;
if (files.length === 0)
return;
$('.upload-modal h1').html(gettext('Uploading…'));
$('.upload-modal .file-name').html(files[0].name);
$('.upload-modal .choose-file-button').hide();
$('.upload-modal .progress-bar').removeClass('loaded').show();
}
function resetUploadBar() {
var percentVal = '0%';
$('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal);
}
function resetUploadModal() {
// Reset modal so it no longer displays information about previously
// completed uploads.
resetUploadBar();
$('.upload-modal .file-name').html('');
$('.upload-modal h1').html(gettext('Upload New File'));
$('.upload-modal .choose-file-button').html(gettext('Choose File'));
$('.upload-modal .embeddable-xml-input').val('');
$('.upload-modal .embeddable').hide();
}
function showUploadFeedback(event, percentComplete) {
var percentVal = percentComplete + '%';
$('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal);
}
function displayFinishedUpload(resp) {
if (resp.status == 200) {
markAsLoaded();
}
$('.upload-modal .embeddable-xml-input').val(resp.portable_url);
$('.upload-modal .embeddable').show();
$('.upload-modal .file-name').hide();
$('.upload-modal .progress-fill').html(resp.msg);
$('.upload-modal .choose-file-button').html(gettext('Load Another File')).show();
$('.upload-modal .progress-fill').width('100%');
// see if this id already exists, if so, then user must have updated an existing piece of content
$("tr[data-id='" + resp.url + "']").remove();
var template = $('#new-asset-element').html();
var html = Mustache.to_html(template, resp);
$('table > tbody').prepend(html);
// re-bind the listeners to delete it
$('.remove-asset-button').bind('click', removeAsset);
analytics.track('Uploaded a File', {
'course': course_location_analytics,
'asset_url': resp.url
});
}
CMS.Views.Assets = Backbone.View.extend({
// takes CMS.Models.AssetCollection as model
initialize : function() {
this.listenTo(this.collection, 'destroy', this.handleDestroy);
this.render();
},
render: function() {
this.$el.empty();
var self = this;
_.each(this.collection.models,
function(asset) {
var view = new CMS.Views.Asset({model: asset});
self.$el.append(view.render().el);
});
return this;
},
handleDestroy: function(model, collection, options) {
var index = options.index;
this.$el.children().eq(index).remove();
analytics.track('Deleted Asset', {
'course': course_location_analytics,
'id': model.get('url')
});
},
addAsset: function (model) {
// If asset is not already being shown, add it.
if (_.all(
this.collection.models,
function (asset) {
return asset.get('url') !== model.get('url');
})) {
this.collection.add(model, {at: 0});
var view = new CMS.Views.Asset({model: model});
this.$el.prepend(view.render().el);
analytics.track('Uploaded a File', {
'course': course_location_analytics,
'asset_url': model.get('url')
});
}
}
});
......@@ -60,7 +60,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
var imageURL = this.model.get('course_image_asset_path');
this.$el.find('#course-image-url').val(imageURL)
this.$el.find('#course-image-url').val(imageURL);
this.$el.find('#course-image').attr('src', imageURL);
return this;
......@@ -262,9 +262,9 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
model: upload,
onSuccess: function(response) {
var options = {
'course_image_name': response.displayname,
'course_image_asset_path': response.url
}
'course_image_name': response.asset.display_name,
'course_image_asset_path': response.asset.url
};
self.model.set(options);
self.render();
$('#course-image').attr('src', self.model.get('course_image_asset_path'))
......
......@@ -248,11 +248,11 @@ CMS.Views.EditChapter = Backbone.View.extend({
onSuccess: function(response) {
var options = {};
if(!that.model.get('name')) {
options.name = response.displayname;
options.name = response.asset.display_name;
}
options.asset_path = response.url;
options.asset_path = response.asset.url;
that.model.set(options);
},
}
});
$(".wrapper-view").after(view.show().el);
}
......
......@@ -6,39 +6,113 @@
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
<script type="text/template" id="asset-tpl">
<%static:include path="js/asset.underscore"/>
</script>
</%block>
<%block name="jsextra">
<script src="${static.url('js/vendor/mustache.js')}"></script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js')}"> </script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"> </script>
<script src="${static.url('js/vendor/mustache.js')}"></script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js')}"> </script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"> </script>
<script type="text/javascript">
CMS.URL.UPDATE_ASSET = "${update_asset_callback_url}";
var assets = new CMS.Models.AssetCollection(${asset_list});
var assetsView = new CMS.Views.Assets({collection: assets, el: $('#asset_table_body')});
$(document).ready(function() {
$('.uploads .upload-button').bind('click', showUploadModal);
$('.upload-modal .close-button').bind('click', hideModal);
$('.upload-modal .choose-file-button').bind('click', showFileSelectionMenu);
});
var showUploadModal = function (e) {
e.preventDefault();
resetUploadModal();
// $modal has to be global for hideModal to work.
$modal = $('.upload-modal').show();
$('.file-input').bind('change', startUpload);
$('.upload-modal .file-chooser').fileupload({
dataType: 'json',
type: 'POST',
maxChunkSize: 100 * 1000 * 1000, // 100 MB
autoUpload: true,
progressall: function(e, data) {
var percentComplete = parseInt((100 * data.loaded) / data.total, 10);
showUploadFeedback(e, percentComplete);
},
maxFileSize: 100 * 1000 * 1000, // 100 MB
maxNumberofFiles: 100,
add: function(e, data) {
data.process().done(function () {
data.submit();
});
},
done: function(e, data) {
displayFinishedUpload(data.result);
}
});
$modalCover.show();
};
var showFileSelectionMenu = function(e) {
e.preventDefault();
$('.file-input').click();
};
var startUpload = function (e) {
var file = e.target.value;
$('.upload-modal h1').html(gettext('Uploading…'));
$('.upload-modal .file-name').html(file.substring(file.lastIndexOf("\\") + 1));
$('.upload-modal .choose-file-button').hide();
$('.upload-modal .progress-bar').removeClass('loaded').show();
};
var resetUploadModal = function () {
// Reset modal so it no longer displays information about previously
// completed uploads.
var percentVal = '0%';
$('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal);
$('.upload-modal .progress-bar').hide();
$('.upload-modal .file-name').show();
$('.upload-modal .file-name').html('');
$('.upload-modal .choose-file-button').html(gettext('Choose File'));
$('.upload-modal .embeddable-xml-input').val('');
$('.upload-modal .embeddable').hide();
};
var showUploadFeedback = function (event, percentComplete) {
var percentVal = percentComplete + '%';
$('.upload-modal .progress-fill').width(percentVal);
$('.upload-modal .progress-fill').html(percentVal);
};
var displayFinishedUpload = function (resp) {
var asset = resp.asset;
$('.upload-modal h1').html(gettext('Upload New File'));
$('.upload-modal .embeddable-xml-input').val(asset.portable_url);
$('.upload-modal .embeddable').show();
$('.upload-modal .file-name').hide();
$('.upload-modal .progress-fill').html(resp.msg);
$('.upload-modal .choose-file-button').html(gettext('Load Another File')).show();
$('.upload-modal .progress-fill').width('100%');
assetsView.addAsset(new CMS.Models.Asset(asset));
};
</script>
</%block>
<%block name="content">
<script type="text/template" id="new-asset-element">
<tr data-id='{{url}}'>
<td class="thumb-col">
<div class="thumb">
{{#thumb_url}}
<img src="{{thumb_url}}">
{{/thumb_url}}
</div>
</td>
<td class="name-col">
<a data-tooltip="Open/download this file" href="{{url}}" class="filename">{{displayname}}</a>
<div class="embeddable-xml"></div>
</td>
<td class="date-col">
{{uploadDate}}
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value='{{portable_url}}' readonly>
</td>
<td class="delete-col">
<a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a>
</td>
</tr>
</script>
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
......@@ -62,7 +136,7 @@
<div class="page-actions">
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
</div>
<article class="asset-library" data-remove-asset-callback-url='${remove_asset_callback_url}'>
<article class="asset-library">
<table>
<thead>
<tr>
......@@ -73,31 +147,8 @@
<th class="delete-col"></th>
</tr>
</thead>
<tbody id="asset_table_body">
% for asset in assets:
<tr data-id="${asset['url']}">
<td class="thumb-col">
<div class="thumb">
% if asset['thumb_url'] is not None:
<img src="${asset['thumb_url']}">
% endif
</div>
</td>
<td class="name-col">
<a data-tooltip="Open/download this file" href="${asset['url']}" class="filename">${asset['displayname']}</a>
<div class="embeddable-xml"></div>
</td>
<td class="date-col">
${asset['uploadDate']}
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value="${asset['portable_url']}" readonly>
</td>
<td class="delete-col">
<a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a>
</td>
</tr>
% endfor
<tbody id="asset_table_body" >
</tbody>
</table>
<nav class="pagination wip-box">
......
<td class="thumb-col">
<div class="thumb">
<% if (thumbnail !== '') { %>
<img src="<%= thumbnail %>">
<% } %>
</div>
</td>
<td class="name-col">
<a data-tooltip="<%= gettext('Open/download this file') %>" href="<%= url %>" class="filename"><%= display_name %></a>
<div class="embeddable-xml"></div>
</td>
<td class="date-col">
<%= date_added %>
</td>
<td class="embed-col">
<input type="text" class="embeddable-xml-input" value="<%= portable_url %>" readonly>
</td>
<td class="delete-col">
<a href="#" data-tooltip="<%= gettext('Delete this asset') %>" class="remove-asset-button"><span
class="delete-icon"></span></a>
</td>
......@@ -76,8 +76,8 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$',
'contentstore.views.asset_index', name='asset_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/remove$',
'contentstore.views.assets.remove_asset', name='remove_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/update/(?P<asset_id>.+)?.*$',
'contentstore.views.assets.update_asset', name='update_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$',
'contentstore.views.textbook_index', name='textbook_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$',
......
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