Commit 42f286b3 by Christina Roberts

Merge pull request #1069 from edx/christina/lock-assets

Add asset locking to the Studio asset page.
parents 190b4183 2cbdaaee
......@@ -7,6 +7,8 @@ the top. Include a label indicating the component affected.
LMS: Add split testing functionality for internal use.
Studio and LMS: add ability to lock assets (cannot be viewed unless registered for class).
LMS: Improved accessibility of parts of forum navigation sidebar.
LMS: enhanced accessibility labeling and aria support for the discussion forum new post dropdown as well as response and comment area labeling.
......
......@@ -300,3 +300,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 + '@edx.org')
login_form.find_by_name('password').fill("test")
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 + '@edx.org')
@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 + '@edx.org'
user = create_studio_user(uname=name, password="test", 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,9 @@
#pylint: disable=W0621
from lettuce import world, step
from common import create_studio_user
from django.contrib.auth.models import Group
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 +13,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'
......@@ -43,7 +20,7 @@ def add_other_user(_step, name):
world.wait(0.5)
email_css = 'input#user-email-input'
world.css_fill(email_css, name + EMAIL_EXTENSION)
world.css_fill(email_css, name + '@edx.org')
if world.is_firefox():
world.trigger_event(email_css)
confirm_css = 'form.create-user button.action-primary'
......@@ -53,7 +30,7 @@ def add_other_user(_step, name):
@step(u'I delete "([^"]*)" from the course team')
def delete_other_user(_step, name):
to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format(
email="{0}{1}".format(name, EMAIL_EXTENSION))
email="{0}{1}".format(name, '@edx.org'))
world.css_click(to_delete_css)
# confirm prompt
# need to wait for the animation to be done, there isn't a good success condition that won't work both on latest chrome and jenkins
......@@ -74,7 +51,7 @@ def other_delete_self(_step):
@step(u'I make "([^"]*)" a course team admin')
def make_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format(
email=name+EMAIL_EXTENSION)
email=name+'@edx.org')
world.css_click(admin_btn_css)
......@@ -83,31 +60,12 @@ def remove_course_team_admin(_step, outer_capture, name):
if outer_capture == "myself":
email = world.scenario_dict["USER"].email
else:
email = name + EMAIL_EXTENSION
email = name + '@edx.org'
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .remove-admin-role'.format(
email=email)
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'):
......@@ -123,7 +81,7 @@ def see_course(_step, do_not_see, gender='self'):
@step(u'"([^"]*)" should( not)? be marked as an admin')
def marked_as_admin(_step, name, not_marked_admin):
flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format(
email=name+EMAIL_EXTENSION)
email=name+'@edx.org')
if not_marked_admin:
assert world.is_css_not_present(flag_css)
else:
......@@ -161,7 +119,7 @@ def can_make_course_admin(_step, can_not_make_admin, outer_capture, name):
if outer_capture == "myself":
email = world.scenario_dict["USER"].email
else:
email = name + EMAIL_EXTENSION
email = name + '@edx.org'
add_button_css = '.user-item[data-email="{email}"] .add-admin-role'.format(email=email)
if can_not_make_admin:
assert world.is_css_not_present(add_button_css)
......
......@@ -58,3 +58,61 @@ 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
# TODO: work with Jay
# @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" can be clicked from the asset index
# 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"
# Note that logging out doesn't really matter at the moment-
# the asset will be protected because the user sent to middleware is the anonymous user.
# Need to work with Jay.
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
And "staff" logs in
Then the asset "locked.html" can be clicked from the asset index
# 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" can be clicked from the asset index
And I log out
Then the asset "unlocked.html" is viewable
......@@ -11,9 +11,10 @@ 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')
@step(u'I go to the files and uploads page$')
def go_to_uploads(_step):
menu_css = 'li.nav-course-courseware'
uploads_css = 'li.nav-course-courseware-uploads a'
......@@ -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,29 +105,96 @@ 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 = "input.lock-checkbox"
world.css_find(lock_css)[index].click()
@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 = "input.lock-checkbox"
checked = world.css_find(lock_css)[index]._element.get_attribute('checked')
assert_equal(lock_state == "locked", bool(checked))
@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)
_verify_body_text()
else:
error_thrown = False
try:
world.visit(url)
except Exception as e:
assert e.status_code == 403
error_thrown = True
assert error_thrown
@step(u'Then the asset "([^"]*)" can be clicked from the asset index$')
def click_asset_from_index(step, file_name):
# This is not ideal, but I'm having trouble with the middleware not having
# the same user in the request when I hit the URL directly.
course_link_css = 'a.course-link'
world.css_click(course_link_css)
step.given("I go to the files and uploads page")
index = get_index(file_name)
assert index != -1
world.css_click('a.filename', index=index)
_verify_body_text()
def _verify_body_text():
def verify_text(driver):
return world.css_text('body') == 'test file'
world.wait_for(verify_text)
@step('I see a confirmation that the file was deleted')
@step('I see a confirmation that the file was deleted$')
def i_see_a_delete_confirmation(_step):
alert_css = '#notification-confirmation'
assert world.is_css_present(alert_css)
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,35 +137,39 @@ 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)
"""
get_location_and_verify_access(request, org, course, name)
# make sure the location is valid
def get_asset_location(asset_id):
""" Helper method to get the location (and verify it is valid). """
try:
loc = StaticContent.get_location_from_path(asset_id)
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)
# also make sure the item to delete actually exists
get_location_and_verify_access(request, org, course, name)
if request.method == 'DELETE':
loc = get_asset_location(asset_id)
# Make sure the item to delete actually exists.
try:
content = contentstore().find(loc)
except NotFoundError:
......@@ -191,8 +196,20 @@ def update_asset(request, org, course, name, asset_id):
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']
location = get_asset_location(asset_id)
contentstore().set_attr(location, 'locked', modified_asset['locked'])
# Delete the asset from the cache so we check the lock status the next time it is requested.
del_cached_content(location)
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 +220,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-checkbox").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-checkbox").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 ->
......
......@@ -8,6 +8,6 @@ CMS.Models.Asset = Backbone.Model.extend({
date_added: "",
url: "",
portable_url: "",
is_locked: false
locked: false
}
});
CMS.Views.Asset = Backbone.View.extend({
initialize: function() {
this.template = _.template($("#asset-tpl").text());
this.listenTo(this.model, "change:locked", this.updateLockState);
},
tagName: "tr",
events: {
"click .remove-asset-button": "confirmDelete"
"click .remove-asset-button": "confirmDelete",
"click .lock-checkbox": "lockAsset"
},
render: function() {
var uniqueId = _.uniqueId('lock_asset_');
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'),
uniqueId: uniqueId}));
this.updateLockState();
return this;
},
updateLockState: function () {
var locked_class = "is-locked";
// Add a class of "locked" to the tr element if appropriate,
// and toggle locked state of hidden checkbox.
if (this.model.get('locked')) {
this.$el.addClass(locked_class);
this.$el.find('.lock-checkbox').attr('checked','checked');
}
else {
this.$el.removeClass(locked_class);
this.$el.find('.lock-checkbox').removeAttr('checked');
}
},
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 +76,18 @@ CMS.Views.Asset = Backbone.View.extend({
]
}
}).show();
},
lockAsset: function(e) {
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,121 +3,237 @@
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();
}
.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;
word-wrap: break-word;
td,
th {
padding: 10px 20px;
text-align: left;
@extend %t-copy-sub2;
background-color: $gray-l5;
padding: 0 $baseline ($baseline*.75) $baseline;
vertical-align: middle;
text-align: left;
color: $gray;
}
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);
vertical-align: middle;
text-align: left;
}
tbody {
background: #fff;
box-shadow: 0 2px 2px $shadow-l1;
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;
}
.name-col {
font-size: 14px;
a {
color: $gray-d1;
&:hover {
color: $blue;
}
}
.date-col {
font-size: 12px;
&.is-locked {
background-image: url('../img/bg-micro-stripes.png');
background-position: 0 0;
background-repeat: repeat;
}
&:hover {
background-color: $blue-l5;
.date-col,
.embed-col,
.embed-col .embeddable-xml-input {
color: $gray;
}
}
}
.thumb-col {
padding: ($baseline/2) $baseline;
.thumb {
width: 100px;
}
.date-col {
width: 220px;
img {
width: 100%;
}
}
.embed-col {
width: 250px;
.name-col {
.title {
@extend %t-copy-sub1;
display: inline-block;
max-width: 200px;
overflow: hidden;
}
}
.delete-col {
width: 20px;
.date-col {
@include transition(all $tmg-f2 ease-in-out 0s);
@extend %t-copy-sub2;
color: $gray-l2;
}
.embed-col {
@include transition(all $tmg-f2 ease-in-out 0s);
padding-left: ($baseline*.75);
color: $gray-l2;
.embeddable-xml-input {
@include transition(all $tmg-f2 ease-in-out 0s);
@extend %t-copy-sub2;
box-shadow: none;
border: none;
background: none;
width: 100%;
color: $gray-l2;
&:focus {
background-color: $white;
box-shadow: 0 1px 5px $shadow-l1 inset;
border: 1px solid $gray-l3;
}
}
}
.thumb {
width: 100px;
max-height: 80px;
.actions-col {
padding: ($baseline/2);
text-align: center;
}
}
}
}
img {
width: 100%;
.action-item {
display: inline-block;
margin: ($baseline/4) 0 ($baseline/4) ($baseline/4);
.action-button {
@include transition(all $tmg-f2 ease-in-out 0s);
display: block;
height: ($baseline*1.5);
width: ($baseline*1.5);
border-radius: 3px;
color: $gray-l3;
&:hover {
background-color: $blue;
color: $gray-l6;
}
}
[class^="icon-"] {
display: inline-block;
vertical-align: bottom;
}
.pagination {
float: right;
margin: 15px 10px;
ol, li {
display: inline;
&.action-lock {
position: relative;
}
a {
&.action-lock label {
position: absolute;
top: 0;
right: 0;
height: 30px;
width: 30px;
&:hover {
background-color: $blue;
}
}
&.action-lock .lock-checkbox {
position: absolute;
top: 0;
right: 0;
height: 30px;
width: 30px;
opacity: 0;
}
&.action-lock .lock-checkbox:hover ~ .action-button {
background-color: $blue;
color: $gray-l6;
}
&.action-lock .lock-checkbox ~ .action-button {
.icon-lock {
display: none;
}
.icon-unlock-alt {
display: inline-block;
height: 25px;
padding: 0 4px;
text-align: center;
line-height: 25px;
}
}
&.action-lock .lock-checkbox:checked ~ .action-button {
background-color: $gray;
color: $white;
.icon-lock {
display: inline-block;
}
.icon-unlock-alt {
display: none;
}
}
}
.show-xml {
@include blue-button;
}
}
.upload-modal {
.upload-modal {
display: none;
width: 640px !important;
margin-left: -320px !important;
......@@ -128,6 +244,13 @@ body.course.uploads {
text-align: center;
}
.title {
@extend %t-title3;
float: none;
margin: ($baseline*2) 0 ($baseline*1.5);
font-weight: 300;
}
.file-input {
display: none;
}
......@@ -140,10 +263,11 @@ body.course.uploads {
.progress-bar {
display: none;
width: 350px;
height: 50px;
margin: 30px auto 10px;
border: 1px solid $blue;
width: ($baseline*15);
height: 35px;
margin: ($baseline) auto;
border: 1px solid $green;
border-radius: ($baseline*2);
&.loaded {
border-color: #66b93d;
......@@ -155,52 +279,55 @@ body.course.uploads {
}
.progress-fill {
@extend %t-copy-sub1;
width: 0%;
height: 50px;
background: $blue;
color: #fff;
line-height: 48px;
}
h1 {
float: none;
margin: 40px 0 30px;
font-size: 34px;
font-weight: 300;
height: ($baseline*1.5);
border-radius: ($baseline*2);
background: $green;
padding-top: ($baseline/4);
color: $white;
}
.close-button {
@include white-button;
@include transition(color $tmg-f2 ease-in-out 0s);
position: absolute;
top: 0;
right: 15px;
width: 29px;
height: 29px;
padding: 0 !important;
border-radius: 17px !important;
line-height: 29px;
text-align: center;
right: ($baseline*.75);
border: none;
background: none;
padding: 0;
[class^="icon-"] {
@extend %t-action1;
}
&:hover {
background: none;
box-shadow: none;
color: $blue;
}
}
.embeddable {
display: none;
margin: 30px 0 130px;
margin: ($baseline*1.5) 0 ($baseline*4);
label {
display: block;
margin-bottom: 10px;
margin-bottom: ($baseline/2);
font-weight: 700;
}
}
.embeddable-xml-input {
box-shadow: none;
width: 400px;
width: ($baseline*20);
}
.copy-button {
@include white-button;
display: none;
margin-bottom: 100px;
margin-bottom: ($baseline*5);
}
}
}
<%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 &amp; Uploads")}</%block>
<%block name="bodyclass">is-signedin course uploads</%block>
<%namespace name='static' file='static_content.html'/>
......@@ -27,7 +28,7 @@
<%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>
......@@ -43,47 +44,52 @@
</ul>
</nav>
</header>
</div>
</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">
<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"></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>
<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>
<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>
</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"><span class="close-icon"></span></a>
<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>${_("Upload New File")}</h1>
<h1 class="title">${_("Upload New File")}</h1>
<p class="file-name"></a>
<div class="progress-bar">
<div class="progress-fill"></div>
......@@ -98,9 +104,9 @@
<input type="file" class="file-input" name="file" multiple>
</form>
</div>
</div>
</div>
<div class="modal-cover"></div>
<div class="modal-cover"></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,15 @@
<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 action-delete">
<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>
<li class="action-item action-lock">
<label for="<%= uniqueId %>"><span class="sr"><%= gettext('Lock this asset') %></span></label>
<input type="checkbox" id="<%= uniqueId %>" class="lock-checkbox" data-tooltip="<%= gettext('Lock/unlock file') %>" />
<div class="action-button"><i class="icon-lock"></i><i class="icon-unlock-alt"></i></div>
</li>
</ul>
</td>
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