Merge pull request #1031 from edx/christina/backbone-assets
Backbone model/view for asset list.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1,76 +1,33 @@
|
||||
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,46 +138,38 @@ 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)
|
||||
@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.
|
||||
|
||||
location = request.POST['location']
|
||||
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)
|
||||
|
||||
# make sure the location is valid
|
||||
try:
|
||||
loc = StaticContent.get_location_from_path(location)
|
||||
except InvalidLocationError:
|
||||
loc = StaticContent.get_location_from_path(asset_id)
|
||||
except InvalidLocationError as err:
|
||||
# return a 'Bad Request' to browser as we have a malformed Location
|
||||
response = HttpResponse()
|
||||
response.status_code = 400
|
||||
return response
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
|
||||
# also make sure the item to delete actually exists
|
||||
try:
|
||||
content = contentstore().find(loc)
|
||||
except NotFoundError:
|
||||
response = HttpResponse()
|
||||
response.status_code = 404
|
||||
return response
|
||||
return JsonResponse(status=404)
|
||||
|
||||
# ok, save the content into the trashcan
|
||||
contentstore('trashcan').save(content)
|
||||
@@ -249,13 +184,26 @@ def remove_asset(request, org, course, name):
|
||||
# remove from any caching
|
||||
del_cached_content(thumbnail_content.location)
|
||||
except:
|
||||
pass # OK if this is left dangling
|
||||
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 HttpResponse()
|
||||
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
|
||||
},
|
||||
|
||||
135
cms/static/coffee/spec/views/assets_spec.coffee
Normal file
135
cms/static/coffee/spec/views/assets_spec.coffee
Normal file
@@ -0,0 +1,135 @@
|
||||
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])
|
||||
@collection.url = "update-asset-url"
|
||||
@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'}
|
||||
])
|
||||
@collection.url = "update-asset-url"
|
||||
@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();
|
||||
|
||||
13
cms/static/js/models/asset.js
Normal file
13
cms/static/js/models/asset.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Simple model for an asset.
|
||||
*/
|
||||
CMS.Models.Asset = Backbone.Model.extend({
|
||||
defaults: {
|
||||
display_name: "",
|
||||
thumbnail: "",
|
||||
date_added: "",
|
||||
url: "",
|
||||
portable_url: "",
|
||||
is_locked: false
|
||||
}
|
||||
});
|
||||
3
cms/static/js/models/assets.js
Normal file
3
cms/static/js/models/assets.js
Normal file
@@ -0,0 +1,3 @@
|
||||
CMS.Models.AssetCollection = Backbone.Collection.extend({
|
||||
model : CMS.Models.Asset
|
||||
});
|
||||
57
cms/static/js/views/asset_view.js
Normal file
57
cms/static/js/views/asset_view.js
Normal file
@@ -0,0 +1,57 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -1,147 +0,0 @@
|
||||
$(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
|
||||
});
|
||||
}
|
||||
45
cms/static/js/views/assets_view.js
Normal file
45
cms/static/js/views/assets_view.js
Normal file
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
this.collection.each(
|
||||
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 (this.collection.findWhere({'url': model.get('url')}) === undefined) {
|
||||
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">
|
||||
var assets = new CMS.Models.AssetCollection(${asset_list});
|
||||
assets.url = "${update_asset_callback_url}";
|
||||
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">
|
||||
|
||||
22
cms/templates/js/asset.underscore
Normal file
22
cms/templates/js/asset.underscore
Normal file
@@ -0,0 +1,22 @@
|
||||
<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>[^/]+)/(?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$',
|
||||
|
||||
Reference in New Issue
Block a user