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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user