Add the ability to lock assets.
This commit is contained in:
@@ -18,6 +18,8 @@ logger = getLogger(__name__)
|
||||
from terrain.browser import reset_data
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
PASSWORD = 'test'
|
||||
EMAIL_EXTENSION = '@edx.org'
|
||||
|
||||
|
||||
@step('I (?:visit|access|open) the Studio homepage$')
|
||||
@@ -300,3 +302,48 @@ def upload_file(filename):
|
||||
world.browser.attach_file('file', os.path.abspath(path))
|
||||
button_css = '.upload-dialog .action-upload'
|
||||
world.css_click(button_css)
|
||||
|
||||
|
||||
@step(u'"([^"]*)" logs in$')
|
||||
def other_user_login(step, name):
|
||||
step.given('I log out')
|
||||
world.visit('/')
|
||||
|
||||
signin_css = 'a.action-signin'
|
||||
world.is_css_present(signin_css)
|
||||
world.css_click(signin_css)
|
||||
|
||||
def fill_login_form():
|
||||
login_form = world.browser.find_by_css('form#login_form')
|
||||
login_form.find_by_name('email').fill(name + EMAIL_EXTENSION)
|
||||
login_form.find_by_name('password').fill(PASSWORD)
|
||||
login_form.find_by_name('submit').click()
|
||||
world.retry_on_exception(fill_login_form)
|
||||
assert_true(world.is_css_present('.new-course-button'))
|
||||
world.scenario_dict['USER'] = get_user_by_email(name + EMAIL_EXTENSION)
|
||||
|
||||
|
||||
@step(u'the user "([^"]*)" exists( as a course (admin|staff member|is_staff))?$')
|
||||
def create_other_user(_step, name, has_extra_perms, role_name):
|
||||
email = name + EMAIL_EXTENSION
|
||||
user = create_studio_user(uname=name, password=PASSWORD, email=email)
|
||||
if has_extra_perms:
|
||||
if role_name == "is_staff":
|
||||
user.is_staff = True
|
||||
else:
|
||||
if role_name == "admin":
|
||||
# admins get staff privileges, as well
|
||||
roles = ("staff", "instructor")
|
||||
else:
|
||||
roles = ("staff",)
|
||||
location = world.scenario_dict["COURSE"].location
|
||||
for role in roles:
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
group, __ = Group.objects.get_or_create(name=groupname)
|
||||
user.groups.add(group)
|
||||
user.save()
|
||||
|
||||
|
||||
@step('I log out')
|
||||
def log_out(_step):
|
||||
world.visit('logout')
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import create_studio_user
|
||||
from django.contrib.auth.models import Group
|
||||
from common import EMAIL_EXTENSION
|
||||
from auth.authz import get_course_groupname_for_role, get_user_by_email
|
||||
from nose.tools import assert_true, assert_in # pylint: disable=E0611
|
||||
|
||||
PASSWORD = 'test'
|
||||
EMAIL_EXTENSION = '@edx.org'
|
||||
|
||||
|
||||
@step(u'(I am viewing|s?he views) the course team settings')
|
||||
def view_grading_settings(_step, whom):
|
||||
@@ -18,24 +14,6 @@ def view_grading_settings(_step, whom):
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@step(u'the user "([^"]*)" exists( as a course (admin|staff member))?$')
|
||||
def create_other_user(_step, name, has_extra_perms, role_name):
|
||||
email = name + EMAIL_EXTENSION
|
||||
user = create_studio_user(uname=name, password=PASSWORD, email=email)
|
||||
if has_extra_perms:
|
||||
location = world.scenario_dict["COURSE"].location
|
||||
if role_name == "admin":
|
||||
# admins get staff privileges, as well
|
||||
roles = ("staff", "instructor")
|
||||
else:
|
||||
roles = ("staff",)
|
||||
for role in roles:
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
group, __ = Group.objects.get_or_create(name=groupname)
|
||||
user.groups.add(group)
|
||||
user.save()
|
||||
|
||||
|
||||
@step(u'I add "([^"]*)" to the course team')
|
||||
def add_other_user(_step, name):
|
||||
new_user_css = 'a.create-user-button'
|
||||
@@ -89,25 +67,6 @@ def remove_course_team_admin(_step, outer_capture, name):
|
||||
world.css_click(admin_btn_css)
|
||||
|
||||
|
||||
@step(u'"([^"]*)" logs in$')
|
||||
def other_user_login(_step, name):
|
||||
world.visit('logout')
|
||||
world.visit('/')
|
||||
|
||||
signin_css = 'a.action-signin'
|
||||
world.is_css_present(signin_css)
|
||||
world.css_click(signin_css)
|
||||
|
||||
def fill_login_form():
|
||||
login_form = world.browser.find_by_css('form#login_form')
|
||||
login_form.find_by_name('email').fill(name + EMAIL_EXTENSION)
|
||||
login_form.find_by_name('password').fill(PASSWORD)
|
||||
login_form.find_by_name('submit').click()
|
||||
world.retry_on_exception(fill_login_form)
|
||||
assert_true(world.is_css_present('.new-course-button'))
|
||||
world.scenario_dict['USER'] = get_user_by_email(name + EMAIL_EXTENSION)
|
||||
|
||||
|
||||
@step(u'I( do not)? see the course on my page')
|
||||
@step(u's?he does( not)? see the course on (his|her) page')
|
||||
def see_course(_step, do_not_see, gender='self'):
|
||||
|
||||
@@ -58,3 +58,59 @@ Feature: CMS.Upload Files
|
||||
And I reload the page
|
||||
And I upload the file "test"
|
||||
Then I can download the correct "test" file
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Users can lock assets through asset index
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
When I upload the file "test"
|
||||
And I lock "test"
|
||||
Then "test" is locked
|
||||
And I see a "saving" notification
|
||||
And I reload the page
|
||||
Then "test" is locked
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Users can unlock assets through asset index
|
||||
Given I have opened a course with a locked asset "test"
|
||||
And I unlock "test"
|
||||
Then "test" is unlocked
|
||||
And I see a "saving" notification
|
||||
And I reload the page
|
||||
Then "test" is unlocked
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Locked assets can't be viewed if logged in as unregistered user
|
||||
Given I have opened a course with a locked asset "locked.html"
|
||||
# Then the asset "locked.html" is viewable
|
||||
And the user "bob" exists
|
||||
And "bob" logs in
|
||||
Then the asset "locked.html" is protected
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Locked assets can't be viewed if logged out
|
||||
Given I have opened a course with a locked asset "locked.html"
|
||||
And I log out
|
||||
Then the asset "locked.html" is protected
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Locked assets can be viewed with is_staff account
|
||||
Given I have opened a course with a locked asset "locked.html"
|
||||
And the user "staff" exists as a course is_staff
|
||||
# Then the asset "locked.html" is viewable
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Unlocked assets can be viewed by anyone
|
||||
Given I have opened a course with a unlocked asset "unlocked.html"
|
||||
Then the asset "unlocked.html" is viewable
|
||||
And the user "bob" exists
|
||||
And "bob" logs in
|
||||
Then the asset "unlocked.html" is viewable
|
||||
And I log out
|
||||
Then the asset "unlocked.html" is viewable
|
||||
|
||||
@@ -11,6 +11,7 @@ from nose.tools import assert_equal, assert_not_equal # pylint: disable=E0611
|
||||
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
ASSET_NAMES_CSS = 'td.name-col > span.title > a.filename'
|
||||
|
||||
|
||||
@step(u'I go to the files and uploads page')
|
||||
@@ -59,8 +60,7 @@ def check_not_there(_step, file_name):
|
||||
# the only file that was uploaded, our success criteria
|
||||
# will be that there are no files.
|
||||
# In the future we can refactor if necessary.
|
||||
names_css = 'td.name-col > a.filename'
|
||||
assert(world.is_css_not_present(names_css))
|
||||
assert(world.is_css_not_present(ASSET_NAMES_CSS))
|
||||
|
||||
|
||||
@step(u'I should see the file "([^"]*)" was uploaded$')
|
||||
@@ -88,11 +88,10 @@ def delete_file(_step, file_name):
|
||||
|
||||
@step(u'I should see only one "([^"]*)"$')
|
||||
def no_duplicate(_step, file_name):
|
||||
names_css = 'td.name-col > a.filename'
|
||||
all_names = world.css_find(names_css)
|
||||
all_names = world.css_find(ASSET_NAMES_CSS)
|
||||
only_one = False
|
||||
for i in range(len(all_names)):
|
||||
if file_name == world.css_html(names_css, index=i):
|
||||
if file_name == world.css_html(ASSET_NAMES_CSS, index=i):
|
||||
only_one = not only_one
|
||||
assert only_one
|
||||
|
||||
@@ -106,16 +105,67 @@ def check_download(_step, file_name):
|
||||
downloaded_text = r.text
|
||||
assert cur_text == downloaded_text
|
||||
#resetting the file back to its original state
|
||||
_write_test_file(file_name, "This is an arbitrary file for testing uploads")
|
||||
|
||||
|
||||
def _write_test_file(file_name, text):
|
||||
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
|
||||
#resetting the file back to its original state
|
||||
with open(os.path.abspath(path), 'w') as cur_file:
|
||||
cur_file.write("This is an arbitrary file for testing uploads")
|
||||
cur_file.write(text)
|
||||
|
||||
|
||||
@step(u'I modify "([^"]*)"$')
|
||||
def modify_upload(_step, file_name):
|
||||
new_text = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10))
|
||||
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
|
||||
with open(os.path.abspath(path), 'w') as cur_file:
|
||||
cur_file.write(new_text)
|
||||
_write_test_file(file_name, new_text)
|
||||
|
||||
|
||||
@step(u'I (lock|unlock) "([^"]*)"')
|
||||
def lock_unlock_file(_step, _lock_state, file_name):
|
||||
index = get_index(file_name)
|
||||
assert index != -1
|
||||
lock_css = "a.lock-asset-button"
|
||||
world.css_click(lock_css, index=index)
|
||||
|
||||
|
||||
@step(u'Then "([^"]*)" is (locked|unlocked)')
|
||||
def verify_lock_unlock_file(_step, file_name, lock_state):
|
||||
index = get_index(file_name)
|
||||
assert index != -1
|
||||
lock_css = "a.lock-asset-button"
|
||||
text = (world.css_find(lock_css)[index]).text
|
||||
if lock_state == "locked":
|
||||
assert_equal("Unlock this asset", text)
|
||||
else:
|
||||
assert_equal("Lock this asset", text)
|
||||
|
||||
|
||||
@step(u'I have opened a course with a (locked|unlocked) asset "([^"]*)"')
|
||||
def open_course_with_locked(step, lock_state, file_name):
|
||||
step.given('I have opened a new course in studio')
|
||||
step.given('I go to the files and uploads page')
|
||||
_write_test_file(file_name, "test file")
|
||||
step.given('I upload the file "' + file_name + '"')
|
||||
if lock_state == "locked":
|
||||
step.given('I lock "' + file_name + '"')
|
||||
step.given('I reload the page')
|
||||
|
||||
|
||||
@step(u'Then the asset "([^"]*)" is (viewable|protected)')
|
||||
def view_asset(step, file_name, status):
|
||||
url = '/c4x/MITx/999/asset/' + file_name
|
||||
if status == 'viewable':
|
||||
world.visit(url)
|
||||
assert world.css_text('body') == 'test file'
|
||||
else:
|
||||
error_thrown = False
|
||||
try:
|
||||
world.visit(url)
|
||||
except Exception as e:
|
||||
assert e.status_code == 403
|
||||
error_thrown = True
|
||||
assert error_thrown
|
||||
|
||||
|
||||
@step('I see a confirmation that the file was deleted')
|
||||
@@ -125,10 +175,9 @@ def i_see_a_delete_confirmation(_step):
|
||||
|
||||
|
||||
def get_index(file_name):
|
||||
names_css = 'td.name-col > a.filename'
|
||||
all_names = world.css_find(names_css)
|
||||
all_names = world.css_find(ASSET_NAMES_CSS)
|
||||
for i in range(len(all_names)):
|
||||
if file_name == world.css_html(names_css, index=i):
|
||||
if file_name == world.css_html(ASSET_NAMES_CSS, index=i):
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
import json
|
||||
|
||||
class AssetsTestCase(CourseTestCase):
|
||||
def setUp(self):
|
||||
@@ -92,7 +93,7 @@ class AssetToJsonTestCase(TestCase):
|
||||
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)
|
||||
output = assets._get_asset_json("my_file", upload_date, location, thumbnail_location, True)
|
||||
|
||||
self.assertEquals(output["display_name"], "my_file")
|
||||
self.assertEquals(output["date_added"], "Jun 01, 2013 at 10:30 UTC")
|
||||
@@ -100,6 +101,48 @@ class AssetToJsonTestCase(TestCase):
|
||||
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"])
|
||||
self.assertEquals(output['locked'], True)
|
||||
|
||||
output = assets._get_asset_json("name", upload_date, location, None)
|
||||
output = assets._get_asset_json("name", upload_date, location, None, False)
|
||||
self.assertIsNone(output["thumbnail"])
|
||||
|
||||
|
||||
class LockAssetTestCase(CourseTestCase):
|
||||
"""
|
||||
Unit test for locking and unlocking an asset.
|
||||
"""
|
||||
|
||||
def test_locking(self):
|
||||
"""
|
||||
Tests a simple locking and unlocking of an asset in the toy course.
|
||||
"""
|
||||
def verify_asset_locked_state(locked):
|
||||
""" Helper method to verify lock state in the contentstore """
|
||||
asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/sample_static.txt')
|
||||
content = contentstore().find(asset_location)
|
||||
self.assertEqual(content.locked, locked)
|
||||
|
||||
def post_asset_update(lock):
|
||||
""" Helper method for posting asset update. """
|
||||
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
|
||||
location = Location(['c4x', 'edX', 'toy', 'asset', 'sample_static.txt'])
|
||||
url = reverse('update_asset', kwargs={'org': 'edX', 'course': 'toy', 'name': '2012_Fall'})
|
||||
|
||||
resp = self.client.post(url, json.dumps(assets._get_asset_json("sample_static.txt", upload_date, location, None, lock)), "application/json")
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
return json.loads(resp.content)
|
||||
|
||||
# Load the toy course.
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=contentstore(), verbose=True)
|
||||
verify_asset_locked_state(False)
|
||||
|
||||
# Lock the asset
|
||||
resp_asset = post_asset_update(True)
|
||||
self.assertTrue(resp_asset['locked'])
|
||||
verify_asset_locked_state(True)
|
||||
|
||||
# Unlock the asset
|
||||
resp_asset = post_asset_update(False)
|
||||
self.assertFalse(resp_asset['locked'])
|
||||
verify_asset_locked_state(False)
|
||||
|
||||
@@ -60,7 +60,8 @@ def asset_index(request, org, course, name):
|
||||
_thumbnail_location = asset.get('thumbnail_location', None)
|
||||
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
|
||||
|
||||
asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location))
|
||||
asset_locked = asset.get('locked', False)
|
||||
asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location, asset_locked))
|
||||
|
||||
return render_to_response('asset_index.html', {
|
||||
'context_course': course_module,
|
||||
@@ -136,63 +137,75 @@ def upload_asset(request, org, course, coursename):
|
||||
# readback the saved content - we need the database timestamp
|
||||
readback = contentstore().find(content.location)
|
||||
|
||||
locked = getattr(content, 'locked', False)
|
||||
response_payload = {
|
||||
'asset': _get_asset_json(content.name, readback.last_modified_at, content.location, content.thumbnail_location),
|
||||
'asset': _get_asset_json(content.name, readback.last_modified_at, content.location, content.thumbnail_location, locked),
|
||||
'msg': _('Upload completed')
|
||||
}
|
||||
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@require_http_methods(("DELETE",))
|
||||
@require_http_methods(("DELETE", "POST", "PUT"))
|
||||
@login_required
|
||||
@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.
|
||||
Currently only DELETE, POST, and PUT methods are implemented.
|
||||
|
||||
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)
|
||||
"""
|
||||
def get_asset_location(asset_id):
|
||||
""" Helper method to get the location (and verify it is valid). """
|
||||
try:
|
||||
return 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)
|
||||
|
||||
get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
# make sure the location is valid
|
||||
try:
|
||||
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:
|
||||
if request.method == 'DELETE':
|
||||
loc = get_asset_location(asset_id)
|
||||
# Make sure the item to delete actually exists.
|
||||
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:
|
||||
logging.warning('Could not delete thumbnail: ' + content.thumbnail_location)
|
||||
content = contentstore().find(loc)
|
||||
except NotFoundError:
|
||||
return JsonResponse(status=404)
|
||||
|
||||
# delete the original
|
||||
contentstore().delete(content.get_id())
|
||||
# remove from cache
|
||||
del_cached_content(content.location)
|
||||
return JsonResponse()
|
||||
# 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:
|
||||
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 JsonResponse()
|
||||
|
||||
elif request.method in ('PUT', 'POST'):
|
||||
# We don't support creation of new assets through this
|
||||
# method-- just changing the locked state.
|
||||
modified_asset = json.loads(request.body)
|
||||
asset_id = modified_asset['url']
|
||||
contentstore().set_attr(get_asset_location(asset_id), 'locked', modified_asset['locked'])
|
||||
return JsonResponse(modified_asset, status=201)
|
||||
|
||||
|
||||
def _get_asset_json(display_name, date, location, thumbnail_location):
|
||||
def _get_asset_json(display_name, date, location, thumbnail_location, locked):
|
||||
"""
|
||||
Helper method for formatting the asset information to send to client.
|
||||
"""
|
||||
@@ -203,6 +216,7 @@ def _get_asset_json(display_name, date, location, thumbnail_location):
|
||||
'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,
|
||||
'locked': locked,
|
||||
# Needed for Backbone delete/update.
|
||||
'id': asset_url
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ describe "CMS.Views.Asset", ->
|
||||
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()
|
||||
spyOn(@model, "save").andCallThrough()
|
||||
|
||||
@collection = new CMS.Models.AssetCollection([@model])
|
||||
@collection.url = "update-asset-url"
|
||||
@view = new CMS.Views.Asset({model: @model})
|
||||
@@ -35,7 +37,10 @@ describe "CMS.Views.Asset", ->
|
||||
@xhr = sinon.useFakeXMLHttpRequest()
|
||||
@xhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
|
||||
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Confirmation", ["show"])
|
||||
@confirmationSpies = spyOnConstructor(CMS.Views.Notification, "Confirmation", ["show"])
|
||||
@confirmationSpies.show.andReturn(@confirmationSpies)
|
||||
|
||||
@savingSpies = spyOnConstructor(CMS.Views.Notification, "Mini", ["show", "hide"])
|
||||
@savingSpies.show.andReturn(@savingSpies)
|
||||
|
||||
afterEach ->
|
||||
@@ -49,13 +54,13 @@ describe "CMS.Views.Asset", ->
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
expect(@requests.length).toEqual(1)
|
||||
expect(@savingSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@confirmationSpies.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(@confirmationSpies.constructor).toHaveBeenCalled()
|
||||
expect(@confirmationSpies.show).toHaveBeenCalled()
|
||||
savingOptions = @confirmationSpies.constructor.mostRecentCall.args[0]
|
||||
expect(savingOptions.title).toMatch("Your file has been deleted.")
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
|
||||
@@ -68,9 +73,31 @@ describe "CMS.Views.Asset", ->
|
||||
expect(@model.destroy).toHaveBeenCalled()
|
||||
# return an error response
|
||||
@requests[0].respond(404)
|
||||
expect(@savingSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@confirmationSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeTruthy()
|
||||
|
||||
it "should lock the asset on confirmation", ->
|
||||
@view.render().$(".lock-asset-button").click()
|
||||
# AJAX request has been sent, but not yet returned
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
expect(@requests.length).toEqual(1)
|
||||
expect(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
|
||||
expect(savingOptions.title).toMatch("Saving...")
|
||||
expect(@model.get("locked")).toBeFalsy()
|
||||
# return a success response
|
||||
@requests[0].respond(200)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@model.get("locked")).toBeTruthy()
|
||||
|
||||
it "should not lock the asset if server errors", ->
|
||||
@view.render().$(".lock-asset-button").click()
|
||||
# return an error response
|
||||
@requests[0].respond(404)
|
||||
# Don't call hide because that closes the notification showing the server error.
|
||||
expect(@savingSpies.hide).not.toHaveBeenCalled()
|
||||
expect(@model.get("locked")).toBeFalsy()
|
||||
|
||||
describe "CMS.Views.Assets", ->
|
||||
beforeEach ->
|
||||
|
||||
BIN
cms/static/img/bg-micro-stripes.png
Normal file
BIN
cms/static/img/bg-micro-stripes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 B |
@@ -8,6 +8,6 @@ CMS.Models.Asset = Backbone.Model.extend({
|
||||
date_added: "",
|
||||
url: "",
|
||||
portable_url: "",
|
||||
is_locked: false
|
||||
locked: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
CMS.Views.Asset = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.template = _.template($("#asset-tpl").text());
|
||||
this.listenTo(this.model, "change", this.render);
|
||||
},
|
||||
|
||||
tagName: "tr",
|
||||
|
||||
events: {
|
||||
"click .remove-asset-button": "confirmDelete"
|
||||
"click .remove-asset-button": "confirmDelete",
|
||||
"click .lock-asset-button": "lockAsset"
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.removeClass();
|
||||
|
||||
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')}));
|
||||
portable_url: this.model.get('portable_url'),
|
||||
locked: this.model.get('locked')}));
|
||||
|
||||
// Add a class of "locked" to the tr element if appropriate.
|
||||
if (this.model.get('locked')) {
|
||||
this.$el.addClass('is-locked');
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
confirmDelete: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
var asset = this.model, collection = this.model.collection;
|
||||
var asset = this.model;
|
||||
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)"),
|
||||
@@ -53,5 +64,19 @@ CMS.Views.Asset = Backbone.View.extend({
|
||||
]
|
||||
}
|
||||
}).show();
|
||||
},
|
||||
|
||||
lockAsset: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
var asset = this.model;
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
title: gettext("Saving…")
|
||||
}).show();
|
||||
asset.save({'locked': !asset.get('locked')}, {
|
||||
wait: true, // This means we won't re-render until we get back the success state.
|
||||
success: function() {
|
||||
saving.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,204 +3,331 @@
|
||||
|
||||
body.course.uploads {
|
||||
|
||||
.content-primary, .content-supplementary {
|
||||
@include box-sizing(border-box);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.content-primary {
|
||||
width: flex-grid(9, 12);
|
||||
margin-right: flex-gutter();
|
||||
|
||||
.no-assets-content {
|
||||
@extend %ui-well;
|
||||
padding: ($baseline*2);
|
||||
background-color: $gray-l4;
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
|
||||
.new-button {
|
||||
@extend %t-copy-sub1;
|
||||
margin-left: $baseline;
|
||||
|
||||
[class^="icon-"] {
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
width: flex-grid(3, 12);
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
|
||||
.icon-cloud-upload {
|
||||
@include font-size(16);
|
||||
@extend %t-copy;
|
||||
vertical-align: bottom;
|
||||
margin-right: ($baseline/5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
input.asset-search-input {
|
||||
float: left;
|
||||
width: 260px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.asset-library {
|
||||
@include clearfix;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border: 1px solid #c5cad4;
|
||||
border-top: 5px solid $gray-l4;
|
||||
word-wrap: break-word;
|
||||
|
||||
|
||||
thead tr {
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 10px 20px;
|
||||
@extend %t-copy-sub2;
|
||||
background-color: $gray-l5;
|
||||
color: $gray;
|
||||
padding: ($baseline*.75) $baseline;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
thead th {
|
||||
@include linear-gradient(top, transparent, rgba(0, 0, 0, .1));
|
||||
background-color: #ced2db;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
|
||||
td {
|
||||
padding: ($baseline/2);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tbody {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 5px $shadow;
|
||||
border: 1px solid $gray-l4;
|
||||
background: $white;
|
||||
|
||||
tr {
|
||||
border-top: 1px solid #c5cad4;
|
||||
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||
border-top: 1px solid $gray-l4;
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background-color: $gray-l6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gray-d1;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-locked {
|
||||
background: $gray-l5 url('../img/bg-micro-stripes.png') 0 0 repeat;
|
||||
|
||||
.locked a {
|
||||
background-color: $gray;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-d3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-l5;
|
||||
|
||||
.date-col,
|
||||
.embed-col,
|
||||
.embed-col .embeddable-xml-input {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.thumb-cols {
|
||||
padding: ($baseline/2) $baseline;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.name-cols {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.date-cols {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.embed-cols {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.actions-cols {
|
||||
width: ($baseline*3);
|
||||
padding: ($baseline/2);
|
||||
}
|
||||
|
||||
.thumb-col {
|
||||
overflow: hidden;
|
||||
|
||||
.thumb {
|
||||
width: $baseline*5;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name-col {
|
||||
font-size: 14px;
|
||||
@extend %t-copy-sub1;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.date-col {
|
||||
font-size: 12px;
|
||||
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||
@extend %t-copy-sub2;
|
||||
color: $gray-l2;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-col {
|
||||
width: 100px;
|
||||
}
|
||||
.embed-col {
|
||||
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||
padding-left: ($baseline*.75);
|
||||
color: $gray-l2;
|
||||
|
||||
.date-col {
|
||||
width: 220px;
|
||||
}
|
||||
.embeddable-xml-input {
|
||||
@extend %t-copy-sub2;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
color: $gray-l2;
|
||||
|
||||
.embed-col {
|
||||
width: 250px;
|
||||
}
|
||||
&:focus {
|
||||
background-color: $white;
|
||||
box-shadow: 0 1px 5px $shadow-l1 inset;
|
||||
border: 1px solid $gray-l3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-col {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.embeddable-xml-input {
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 100px;
|
||||
max-height: 80px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
.actions-col {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
float: right;
|
||||
margin: 15px 10px;
|
||||
|
||||
ol, li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
height: 25px;
|
||||
padding: 0 4px;
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.show-xml {
|
||||
@include blue-button;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-modal {
|
||||
display: none;
|
||||
width: 640px !important;
|
||||
margin-left: -320px !important;
|
||||
|
||||
.modal-body {
|
||||
height: auto !important;
|
||||
overflow-y: auto !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
.action-item {
|
||||
display: inline-block;
|
||||
margin: ($baseline/4) 0 ($baseline/4) ($baseline/4);
|
||||
|
||||
.choose-file-button {
|
||||
@include blue-button;
|
||||
padding: 10px 82px 12px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: none;
|
||||
width: 350px;
|
||||
height: 50px;
|
||||
margin: 30px auto 10px;
|
||||
border: 1px solid $blue;
|
||||
|
||||
&.loaded {
|
||||
border-color: #66b93d;
|
||||
|
||||
.progress-fill {
|
||||
background: #66b93d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
width: 0%;
|
||||
height: 50px;
|
||||
background: $blue;
|
||||
color: #fff;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
margin: 40px 0 30px;
|
||||
font-size: 34px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@include white-button;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 15px;
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
padding: 0 !important;
|
||||
border-radius: 17px !important;
|
||||
line-height: 29px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.embeddable {
|
||||
display: none;
|
||||
margin: 30px 0 130px;
|
||||
|
||||
label {
|
||||
.action-button {
|
||||
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
height: ($baseline*1.5);
|
||||
width: ($baseline*1.5);
|
||||
border-radius: 3px;
|
||||
color: $gray-l3;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-l3;
|
||||
color: $gray-l6;
|
||||
}
|
||||
}
|
||||
|
||||
[class^="icon-"] {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.embeddable-xml-input {
|
||||
box-shadow: none;
|
||||
width: 400px;
|
||||
|
||||
.show-xml {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
@include white-button;
|
||||
|
||||
.upload-modal {
|
||||
display: none;
|
||||
margin-bottom: 100px;
|
||||
width: 640px !important;
|
||||
margin-left: -320px !important;
|
||||
|
||||
.modal-body {
|
||||
height: auto !important;
|
||||
overflow-y: auto !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend %t-title3;
|
||||
float: none;
|
||||
margin: ($baseline*2) 0 ($baseline*1.5);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.choose-file-button {
|
||||
@include blue-button;
|
||||
padding: 10px 82px 12px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: none;
|
||||
width: ($baseline*15);
|
||||
height: 35px;
|
||||
margin: ($baseline) auto;
|
||||
border: 1px solid $green;
|
||||
border-radius: ($baseline*2);
|
||||
|
||||
&.loaded {
|
||||
border-color: #66b93d;
|
||||
|
||||
.progress-fill {
|
||||
background: #66b93d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
@extend %t-copy-sub1;
|
||||
width: 0%;
|
||||
height: ($baseline*1.5);
|
||||
border-radius: ($baseline*2);
|
||||
background: $green;
|
||||
padding-top: ($baseline/4);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@include transition(color $tmg-f2 ease-in-out 0s);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 15px;
|
||||
padding: 0 !important;
|
||||
border-radius: 17px !important;
|
||||
line-height: 29px;
|
||||
text-align: center;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
[class^="icon-"] {
|
||||
@extend %t-action1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.embeddable {
|
||||
display: none;
|
||||
margin: 30px 0 130px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.embeddable-xml-input {
|
||||
box-shadow: none;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
@include white-button;
|
||||
display: none;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<%inherit file="base.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%block name="bodyclass">is-signedin course uploads</%block>
|
||||
|
||||
<%block name="title">${_("Files & Uploads")}</%block>
|
||||
<%block name="bodyclass">is-signedin course uploads</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
@@ -27,80 +28,85 @@
|
||||
|
||||
<%block name="content">
|
||||
|
||||
<div class="wrapper-mast wrapper">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<h1 class="page-header">
|
||||
<small class="subtitle">${_("Content")}</small>
|
||||
<span class="sr">> </span>${_("Files & Uploads")}
|
||||
</h1>
|
||||
<h1 class="page-header">
|
||||
<small class="subtitle">${_("Content")}</small>
|
||||
<span class="sr">> </span>${_("Files & Uploads")}
|
||||
</h1>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">${_("Page Actions")}</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button upload-button new-button"><i class="icon-plus"></i> ${_("Upload New File")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<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">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="thumb-col"></th>
|
||||
<th class="name-col">Name</th>
|
||||
<th class="date-col">Date Added</th>
|
||||
<th class="embed-col">URL</th>
|
||||
<th class="delete-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body" >
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pagination wip-box">
|
||||
Page:
|
||||
<ol class="pages">
|
||||
<li>1</li>
|
||||
<li><a href="#">2</a></li>
|
||||
<li><a href="#">3</a></li>
|
||||
<li><a href="#">4</a></li>
|
||||
<li><a href="#">5</a></li>
|
||||
</ol>
|
||||
<a href="#" class="next">»</a>
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">${_("Page Actions")}</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button upload-button new-button"><i class="icon-plus"></i> ${_("Upload New File")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="upload-modal modal">
|
||||
<a href="#" class="close-button"><span class="close-icon"></span></a>
|
||||
<div class="modal-body">
|
||||
<h1>${_("Upload New File")}</h1>
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="asset-library content-primary" role="main">
|
||||
<table>
|
||||
<caption class="sr">${_("List of uploaded files and assets in this course")}</caption>
|
||||
<colgroup>
|
||||
<col class="thumb-cols" />
|
||||
<col class="name-cols" />
|
||||
<col class="date-cols" />
|
||||
<col class="embed-cols" />
|
||||
<col class="actions-cols" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="thumb-col">${_("Preview")}</th>
|
||||
<th class="name-col">${_("Name")}</th>
|
||||
<th class="date-col">${_("Date Added")}</th>
|
||||
<th class="embed-col">${_("URL")}</th>
|
||||
<th class="actions-col"><span class="sr">${_("Actions")}</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body" >
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What files are included here?")}</h3>
|
||||
<p>${_("Any file you upload to the course will be listed here, including your course image, textbook chapters, and any files you add directly to this page.")}</p>
|
||||
</div>
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<p>${_("You can click the file name to view or download the file, upload a new file, delete a file, and lock a file to prevent people who are not enrolled from accessing that specific file. You can also copy the location (URL) of a file to use elsewhere in your course.")}</p>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="upload-modal modal">
|
||||
<a href="#" class="close-button"><i class="icon-remove-sign"></i> <span class="sr">${_('close')}</span></a>
|
||||
<div class="modal-body">
|
||||
<h1 class="title">${_("Upload New File")}</h1>
|
||||
<p class="file-name"></a>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill"></div>
|
||||
<div class="progress-fill"></div>
|
||||
</div>
|
||||
<div class="embeddable">
|
||||
<label>URL:</label>
|
||||
<input type="text" class="embeddable-xml-input" value='' readonly>
|
||||
</div>
|
||||
<div class="embeddable">
|
||||
<label>URL:</label>
|
||||
<input type="text" class="embeddable-xml-input" value='' readonly>
|
||||
</div>
|
||||
<form class="file-chooser" action="${upload_asset_callback_url}"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<a href="#" class="choose-file-button">${_("Choose File")}</a>
|
||||
<input type="file" class="file-input" name="file" multiple>
|
||||
method="post" enctype="multipart/form-data">
|
||||
<a href="#" class="choose-file-button">${_("Choose File")}</a>
|
||||
<input type="file" class="file-input" name="file" multiple>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-cover"></div>
|
||||
<div class="modal-cover"></div>
|
||||
|
||||
|
||||
</%block>
|
||||
@@ -108,17 +114,17 @@
|
||||
<%block name="view_alerts">
|
||||
<!-- alert: save confirmed with close -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-confirmation" role="status">
|
||||
<div class="alert confirmation">
|
||||
<i class="icon-ok"></i>
|
||||
<div class="alert confirmation">
|
||||
<i class="icon-ok"></i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">${_('Your file has been deleted.')}</h2>
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">${_('Your file has been deleted.')}</h2>
|
||||
</div>
|
||||
|
||||
<a href="" rel="view" class="action action-alert-close">
|
||||
<i class="icon-remove-sign"></i>
|
||||
<span class="label">${_('close alert')}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="" rel="view" class="action action-alert-close">
|
||||
<i class="icon-remove-sign"></i>
|
||||
<span class="label">${_('close alert')}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="name-col">
|
||||
<a data-tooltip="<%= gettext('Open/download this file') %>" href="<%= url %>" class="filename"><%= display_name %></a>
|
||||
<span class="title"><a data-tooltip="<%= gettext('Open/download this file') %>" href="<%= url %>" class="filename"><%= display_name %></a></span>
|
||||
|
||||
<div class="embeddable-xml"></div>
|
||||
</td>
|
||||
@@ -16,7 +16,19 @@
|
||||
<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 class="actions-col">
|
||||
<ul>
|
||||
<li class="action-item">
|
||||
<a href="#" data-tooltip="<%= gettext('Delete this asset') %>" class="remove-asset-button action-button"><i class="icon-remove-sign"></i> <span class="sr"><%= gettext('Delete this asset') %></span></a>
|
||||
</li>
|
||||
<% if (locked) { %>
|
||||
<li class="action-item locked">
|
||||
<a href="#" data-tooltip="<%= gettext('Unlock this asset') %>" class="lock-asset-button action-button"><i class="icon-lock"></i> <span class="sr"><%= gettext('Unlock this asset') %></span></a>
|
||||
</li>
|
||||
<% } else { %>
|
||||
<li class="action-item">
|
||||
<a href="#" data-tooltip="<%= gettext('Lock this asset') %>" class="lock-asset-button action-button"><i class="icon-unlock-alt"></i> <span class="sr"><%= gettext('Lock this asset') %></span></a>
|
||||
</li>
|
||||
<% }%>
|
||||
</ul>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user