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 @@ ...@@ -2,7 +2,10 @@
Unit tests for the asset upload endpoint. Unit tests for the asset upload endpoint.
""" """
import json #pylint: disable=C0111
#pylint: disable=W0621
#pylint: disable=W0212
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from pytz import UTC from pytz import UTC
...@@ -12,7 +15,9 @@ from django.core.urlresolvers import reverse ...@@ -12,7 +15,9 @@ from django.core.urlresolvers import reverse
from contentstore.views import assets from contentstore.views import assets
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location 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): class AssetsTestCase(CourseTestCase):
def setUp(self): def setUp(self):
...@@ -27,22 +32,27 @@ class AssetsTestCase(CourseTestCase): ...@@ -27,22 +32,27 @@ class AssetsTestCase(CourseTestCase):
resp = self.client.get(self.url) resp = self.client.get(self.url)
self.assertEquals(resp.status_code, 200) 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): def test_static_url_generation(self):
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg']) location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
path = StaticContent.get_static_path_from_location(location) path = StaticContent.get_static_path_from_location(location)
self.assertEquals(path, '/static/my_file_name.jpg') 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): class UploadTestCase(CourseTestCase):
""" """
Unit tests for uploading a file Unit tests for uploading a file
...@@ -71,32 +81,25 @@ class UploadTestCase(CourseTestCase): ...@@ -71,32 +81,25 @@ class UploadTestCase(CourseTestCase):
self.assertEquals(resp.status_code, 405) 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. we can send out to the client via JSON.
""" """
def test_basic(self): def test_basic(self):
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC) upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
asset = {
"displayname": "foo", location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg'])
"chunkSize": 512, thumbnail_location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name_thumb.jpg'])
"filename": "foo.png",
"length": 100, output = assets._get_asset_json("my_file", upload_date, location, thumbnail_location)
"uploadDate": upload_date,
"_id": { self.assertEquals(output["display_name"], "my_file")
"course": "course", self.assertEquals(output["date_added"], "Jun 01, 2013 at 10:30 UTC")
"org": "org", self.assertEquals(output["url"], "/i4x/foo/bar/asset/my_file_name.jpg")
"revision": 12, self.assertEquals(output["portable_url"], "/static/my_file_name.jpg")
"category": "category", self.assertEquals(output["thumbnail"], "/i4x/foo/bar/asset/my_file_name_thumb.jpg")
"name": "name", self.assertEquals(output["id"], output["url"])
"tag": "tag",
} output = assets._get_asset_json("name", upload_date, location, None)
} self.assertIsNone(output["thumbnail"])
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")
...@@ -593,9 +593,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -593,9 +593,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view # 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'}) url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall', 'asset_id': '/c4x/edX/toy/asset/sample_static.txt'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'}) resp = self.client.delete(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 204)
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt') asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
...@@ -628,7 +628,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -628,7 +628,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_empty_trashcan(self): 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() content_store = contentstore()
trash_store = contentstore('trashcan') trash_store = contentstore('trashcan')
...@@ -644,9 +644,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -644,9 +644,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# go through the website to do the delete, since the soft-delete logic is in the view # 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'}) url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall', 'asset_id': '/c4x/edX/toy/asset/sample_static.txt'})
resp = self.client.post(url, {'location': '/c4x/edX/toy/asset/sample_static.txt'}) resp = self.client.delete(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 204)
# make sure there's something in the trashcan # make sure there's something in the trashcan
all_assets = trash_store.get_all_content_for_course(course_location) all_assets = trash_store.get_all_content_for_course(course_location)
......
...@@ -254,8 +254,10 @@ PIPELINE_JS = { ...@@ -254,8 +254,10 @@ PIPELINE_JS = {
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js', 'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/models/uploads.js', 'js/views/uploads.js', 'js/models/uploads.js', 'js/views/uploads.js',
'js/models/textbook.js', 'js/views/textbook.js', 'js/models/textbook.js', 'js/views/textbook.js',
'js/views/assets.js', 'js/src/utility.js', 'js/src/utility.js',
'js/models/settings/course_grading_policy.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', 'output_filename': 'js/cms-application.js',
'test_order': 0 '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", -> ...@@ -11,15 +11,11 @@ describe "CMS.Views.UploadDialog", ->
@model = new CMS.Models.FileUpload( @model = new CMS.Models.FileUpload(
mimeTypes: ['application/pdf'] mimeTypes: ['application/pdf']
) )
@chapter = new CMS.Models.Chapter() @dialogResponse = dialogResponse = []
@view = new CMS.Views.UploadDialog( @view = new CMS.Views.UploadDialog(
model: @model, model: @model,
onSuccess: (response) => onSuccess: (response) =>
options = {} dialogResponse.push(response.response)
if !@chapter.get('name')
options.name = response.displayname
options.asset_path = response.url
@chapter.set(options)
) )
spyOn(@view, 'remove').andCallThrough() spyOn(@view, 'remove').andCallThrough()
...@@ -66,7 +62,6 @@ describe "CMS.Views.UploadDialog", -> ...@@ -66,7 +62,6 @@ describe "CMS.Views.UploadDialog", ->
expect(@view.$el).toContain("#upload_error") expect(@view.$el).toContain("#upload_error")
expect(@view.$(".action-upload")).toHaveClass("disabled") expect(@view.$(".action-upload")).toHaveClass("disabled")
it "adds body class on show()", -> it "adds body class on show()", ->
@view.show() @view.show()
expect(@view.options.shown).toBeTruthy() expect(@view.options.shown).toBeTruthy()
...@@ -99,11 +94,10 @@ describe "CMS.Views.UploadDialog", -> ...@@ -99,11 +94,10 @@ describe "CMS.Views.UploadDialog", ->
expect(request.method).toEqual("POST") expect(request.method).toEqual("POST")
request.respond(200, {"Content-Type": "application/json"}, request.respond(200, {"Content-Type": "application/json"},
'{"displayname": "starfish", "url": "/uploaded/starfish.pdf"}') '{"response": "dummy_response"}')
expect(@model.get("uploading")).toBeFalsy() expect(@model.get("uploading")).toBeFalsy()
expect(@model.get("finished")).toBeTruthy() expect(@model.get("finished")).toBeTruthy()
expect(@chapter.get("name")).toEqual("starfish") expect(@dialogResponse.pop()).toEqual("dummy_response")
expect(@chapter.get("asset_path")).toEqual("/uploaded/starfish.pdf")
it "can handle upload errors", -> it "can handle upload errors", ->
@view.upload() @view.upload()
...@@ -114,7 +108,7 @@ describe "CMS.Views.UploadDialog", -> ...@@ -114,7 +108,7 @@ describe "CMS.Views.UploadDialog", ->
it "removes itself after two seconds on successful upload", -> it "removes itself after two seconds on successful upload", ->
@view.upload() @view.upload()
@requests[0].respond(200, {"Content-Type": "application/json"}, @requests[0].respond(200, {"Content-Type": "application/json"},
'{"displayname": "starfish", "url": "/uploaded/starfish.pdf"}') '{"response": "dummy_response"}')
expect(@view.remove).not.toHaveBeenCalled() expect(@view.remove).not.toHaveBeenCalled()
@clock.tick(2001) @clock.tick(2001)
expect(@view.remove).toHaveBeenCalled() expect(@view.remove).toHaveBeenCalled()
...@@ -421,11 +421,6 @@ function _deleteItem($el, type) { ...@@ -421,11 +421,6 @@ function _deleteItem($el, type) {
confirm.show(); confirm.show();
} }
function markAsLoaded() {
$('.upload-modal .copy-button').css('display', 'inline-block');
$('.upload-modal .progress-bar').addClass('loaded');
}
function hideModal(e) { function hideModal(e) {
if (e) { if (e) {
e.preventDefault(); 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({ ...@@ -60,7 +60,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort')); this.$el.find('#' + this.fieldToSelectorMap['effort']).val(this.model.get('effort'));
var imageURL = this.model.get('course_image_asset_path'); 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); this.$el.find('#course-image').attr('src', imageURL);
return this; return this;
...@@ -262,9 +262,9 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -262,9 +262,9 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
model: upload, model: upload,
onSuccess: function(response) { onSuccess: function(response) {
var options = { var options = {
'course_image_name': response.displayname, 'course_image_name': response.asset.display_name,
'course_image_asset_path': response.url 'course_image_asset_path': response.asset.url
} };
self.model.set(options); self.model.set(options);
self.render(); self.render();
$('#course-image').attr('src', self.model.get('course_image_asset_path')) $('#course-image').attr('src', self.model.get('course_image_asset_path'))
......
...@@ -248,11 +248,11 @@ CMS.Views.EditChapter = Backbone.View.extend({ ...@@ -248,11 +248,11 @@ CMS.Views.EditChapter = Backbone.View.extend({
onSuccess: function(response) { onSuccess: function(response) {
var options = {}; var options = {};
if(!that.model.get('name')) { 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); that.model.set(options);
}, }
}); });
$(".wrapper-view").after(view.show().el); $(".wrapper-view").after(view.show().el);
} }
......
...@@ -6,39 +6,113 @@ ...@@ -6,39 +6,113 @@
<%namespace name='static' file='static_content.html'/> <%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"> <%block name="jsextra">
<script src="${static.url('js/vendor/mustache.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.iframe-transport.js')}"> </script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.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>
<%block name="content"> <%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"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
...@@ -62,7 +136,7 @@ ...@@ -62,7 +136,7 @@
<div class="page-actions"> <div class="page-actions">
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/> <input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
</div> </div>
<article class="asset-library" data-remove-asset-callback-url='${remove_asset_callback_url}'> <article class="asset-library">
<table> <table>
<thead> <thead>
<tr> <tr>
...@@ -73,31 +147,8 @@ ...@@ -73,31 +147,8 @@
<th class="delete-col"></th> <th class="delete-col"></th>
</tr> </tr>
</thead> </thead>
<tbody id="asset_table_body"> <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> </tbody>
</table> </table>
<nav class="pagination wip-box"> <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 ...@@ -76,8 +76,8 @@ urlpatterns = ('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$',
'contentstore.views.asset_index', name='asset_index'), 'contentstore.views.asset_index', name='asset_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/remove$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/update/(?P<asset_id>.+)?.*$',
'contentstore.views.assets.remove_asset', name='remove_asset'), 'contentstore.views.assets.update_asset', name='update_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$',
'contentstore.views.textbook_index', name='textbook_index'), 'contentstore.views.textbook_index', name='textbook_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$', 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