Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.

Studio: Added pagination to the Files & Uploads page.

Blades: Video player improvements:
- Disable edX controls on iPhone/iPod (native controls are used).
- Disable unsupported controls (volume, playback rate) on iPad/Android.
Expand Down
116 changes: 30 additions & 86 deletions cms/djangoapps/contentstore/tests/test_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_static_url_generation(self):

class AssetsToyCourseTestCase(CourseTestCase):
"""
Tests the assets returned from assets_handler (full page content) for the toy test course.
Tests the assets returned from assets_handler for the toy test course.
"""
def test_toy_assets(self):
module_store = modulestore('direct')
Expand All @@ -56,10 +56,35 @@ def test_toy_assets(self):
location = loc_mapper().translate_location(course.location.course_id, course.location, False, True)
url = location.url_reverse('assets/', '')

resp = self.client.get(url, HTTP_ACCEPT='text/html')
# Test a small portion of the asset data passed to the client.
self.assertContains(resp, "new AssetCollection([{")
self.assertContains(resp, "/c4x/edX/toy/asset/handouts_sample_handout.txt")
self.assert_correct_asset_response(url, 0, 3, 3)
self.assert_correct_asset_response(url + '?page_size=2', 0, 2, 3)
self.assert_correct_asset_response(url + '?page_size=2&page=1', 2, 1, 3)
self.assert_correct_sort_response(url, 'date_added', 'asc')
self.assert_correct_sort_response(url, 'date_added', 'desc')
self.assert_correct_sort_response(url, 'display_name', 'asc')
self.assert_correct_sort_response(url, 'display_name', 'desc')

def assert_correct_asset_response(self, url, expected_start, expected_length, expected_total):
resp = self.client.get(url, HTTP_ACCEPT='application/json')
json_response = json.loads(resp.content)
assets = json_response['assets']
self.assertEquals(json_response['start'], expected_start)
self.assertEquals(len(assets), expected_length)
self.assertEquals(json_response['totalCount'], expected_total)

def assert_correct_sort_response(self, url, sort, direction):
resp = self.client.get(url + '?sort=' + sort + '&direction=' + direction, HTTP_ACCEPT='application/json')
json_response = json.loads(resp.content)
assets = json_response['assets']
name1 = assets[0][sort]
name2 = assets[1][sort]
name3 = assets[2][sort]
if direction == 'asc':
self.assertLessEqual(name1, name2)
self.assertLessEqual(name2, name3)
else:
self.assertGreaterEqual(name1, name2)
self.assertGreaterEqual(name2, name3)


class UploadTestCase(CourseTestCase):
Expand All @@ -82,10 +107,6 @@ def test_no_file(self):
resp = self.client.post(self.url, {"name": "file.txt"}, "application/json")
self.assertEquals(resp.status_code, 400)

def test_get(self):
with self.assertRaises(NotImplementedError):
self.client.get(self.url)


class AssetToJsonTestCase(TestCase):
"""
Expand Down Expand Up @@ -163,80 +184,3 @@ def post_asset_update(lock):
resp_asset = post_asset_update(False)
self.assertFalse(resp_asset['locked'])
verify_asset_locked_state(False)


class TestAssetIndex(CourseTestCase):
"""
Test getting asset lists via http (Note, the assets don't actually exist)
"""
def setUp(self):
"""
Create fake asset entries for the other tests to use
"""
super(TestAssetIndex, self).setUp()
self.entry_filter = self.create_asset_entries(contentstore(), 100)
location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
self.url = location.url_reverse('assets/', '')

def tearDown(self):
"""
Get rid of the entries
"""
contentstore().fs_files.remove(self.entry_filter)

def create_asset_entries(self, cstore, number):
"""
Create the fake entries
"""
course_filter = Location(
XASSET_LOCATION_TAG, category='asset', course=self.course.location.course, org=self.course.location.org
)
# purge existing entries (a bit brutal but hopefully tests are independent enuf to not trip on this)
cstore.fs_files.remove(location_to_query(course_filter))
base_entry = {
'displayname': 'foo.jpg',
'chunkSize': 262144,
'length': 0,
'uploadDate': datetime(2012, 1, 2, 0, 0),
'contentType': 'image/jpeg',
}
for i in range(number):
base_entry['displayname'] = '{:03x}.jpeg'.format(i)
base_entry['uploadDate'] += timedelta(hours=i)
base_entry['_id'] = course_filter.replace(name=base_entry['displayname']).dict()
cstore.fs_files.insert(base_entry)

return course_filter.dict()

ASSET_LIST_RE = re.compile(r'AssetCollection\((.*)\);$', re.MULTILINE)

def check_page_content(self, resp_content, entry_count, last_date=None):
"""
:param entry_count:
:param last_date:
"""
match = self.ASSET_LIST_RE.search(resp_content)
asset_list = json.loads(match.group(1))
self.assertEqual(len(asset_list), entry_count)
for row in asset_list:
datetext = row['date_added']
parsed_date = datetime.strptime(datetext, "%b %d, %Y at %H:%M UTC")
if last_date is None:
last_date = parsed_date
else:
self.assertGreaterEqual(last_date, parsed_date)
return last_date

def test_query_assets(self):
"""
The actual test
"""
# get all
resp = self.client.get(self.url, HTTP_ACCEPT='text/html')
self.check_page_content(resp.content, 100)
# get first page of 10
resp = self.client.get(self.url + "?max=10", HTTP_ACCEPT='text/html')
last_date = self.check_page_content(resp.content, 10)
# get next of 20
resp = self.client.get(self.url + "?start=10&max=20", HTTP_ACCEPT='text/html')
self.check_page_content(resp.content, 20, last_date)
12 changes: 7 additions & 5 deletions cms/djangoapps/contentstore/tests/test_contentstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def _lock_an_asset(self, content_store, course_location):
Lock an arbitrary asset in the course
:param course_location:
"""
course_assets = content_store.get_all_content_for_course(course_location)
course_assets, __ = content_store.get_all_content_for_course(course_location)
self.assertGreater(len(course_assets), 0, "No assets to lock")
content_store.set_attr(course_assets[0]['_id'], 'locked', True)
return course_assets[0]['_id']
Expand Down Expand Up @@ -585,7 +585,7 @@ def test_asset_import(self):
self.assertIsNotNone(course)

# make sure we have some assets in our contentstore
all_assets = content_store.get_all_content_for_course(course_location)
all_assets, __ = content_store.get_all_content_for_course(course_location)
self.assertGreater(len(all_assets), 0)

# make sure we have some thumbnails in our contentstore
Expand Down Expand Up @@ -698,7 +698,7 @@ def test_empty_trashcan(self):

# make sure there's something in the trashcan
course_location = CourseDescriptor.id_to_location('edX/toy/6.002_Spring_2012')
all_assets = trash_store.get_all_content_for_course(course_location)
all_assets, __ = trash_store.get_all_content_for_course(course_location)
self.assertGreater(len(all_assets), 0)

# make sure we have some thumbnails in our trashcan
Expand All @@ -713,8 +713,9 @@ def test_empty_trashcan(self):
empty_asset_trashcan([course_location])

# make sure trashcan is empty
all_assets = trash_store.get_all_content_for_course(course_location)
all_assets, count = trash_store.get_all_content_for_course(course_location)
self.assertEqual(len(all_assets), 0)
self.assertEqual(count, 0)

all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location)
self.assertEqual(len(all_thumbnails), 0)
Expand Down Expand Up @@ -923,8 +924,9 @@ def test_delete_course(self):
self.assertEqual(len(items), 0)

# assert that all content in the asset library is also deleted
assets = content_store.get_all_content_for_course(location)
assets, count = content_store.get_all_content_for_course(location)
self.assertEqual(len(assets), 0)
self.assertEqual(count, 0)

def verify_content_existence(self, store, root_dir, location, dirname, category_name, filename_suffix=''):
filesystem = OSFS(root_dir / 'test_export')
Expand Down
6 changes: 4 additions & 2 deletions cms/djangoapps/contentstore/tests/test_import_nostatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ def test_static_import(self):
_, content_store, course, course_location = self.load_test_import_course()

# make sure we have ONE asset in our contentstore ("should_be_imported.html")
all_assets = content_store.get_all_content_for_course(course_location)
all_assets, count = content_store.get_all_content_for_course(course_location)
print "len(all_assets)=%d" % len(all_assets)
self.assertEqual(len(all_assets), 1)
self.assertEqual(count, 1)

content = None
try:
Expand Down Expand Up @@ -114,9 +115,10 @@ def test_asset_import_nostatic(self):
module_store.get_item(course_location)

# make sure we have NO assets in our contentstore
all_assets = content_store.get_all_content_for_course(course_location)
all_assets, count = content_store.get_all_content_for_course(course_location)
print "len(all_assets)=%d" % len(all_assets)
self.assertEqual(len(all_assets), 0)
self.assertEqual(count, 0)

def test_no_static_link_rewrites_on_import(self):
module_store = modulestore('direct')
Expand Down
76 changes: 52 additions & 24 deletions cms/djangoapps/contentstore/views/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from django.http import HttpResponseNotFound
import json
from django.utils.translation import ugettext as _
from pymongo import DESCENDING
from pymongo import ASCENDING, DESCENDING


__all__ = ['assets_handler']
Expand All @@ -41,9 +41,13 @@ def assets_handler(request, tag=None, package_id=None, branch=None, version_guid
deleting assets, and changing the "locked" state of an asset.

GET
html: return html page of all course assets (note though that a range of assets can be requested using start
and max query parameters)
json: not currently supported
html: return an html page which will show all course assets. Note that only the asset container
is returned and that the actual assets are filled in with a client-side request.
json: returns a page of assets. The following parameters are supported:
page: the desired page of results (defaults to 0)
page_size: the number of items per page (defaults to 50)
sort: the asset field to sort by (defaults to "date_added")
direction: the sort direction (defaults to "descending")
POST
json: create (or update?) an asset. The only updating that can be done is changing the lock state.
PUT
Expand All @@ -55,9 +59,10 @@ def assets_handler(request, tag=None, package_id=None, branch=None, version_guid
if not has_access(request.user, location):
raise PermissionDenied()

if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
response_format = request.REQUEST.get('format', 'html')
if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
if request.method == 'GET':
raise NotImplementedError('coming soon')
return _assets_json(request, location)
else:
return _update_asset(request, location, asset_id)
elif request.method == 'GET': # assume html
Expand All @@ -73,22 +78,42 @@ def _asset_index(request, location):
Supports start (0-based index into the list of assets) and max query parameters.
"""
old_location = loc_mapper().translate_locator_to_location(location)

course_module = modulestore().get_item(old_location)
maxresults = request.REQUEST.get('max', None)
start = request.REQUEST.get('start', None)

return render_to_response('asset_index.html', {
'context_course': course_module,
'asset_callback_url': location.url_reverse('assets/', '')
})


def _assets_json(request, location):
"""
Display an editable asset library.

Supports start (0-based index into the list of assets) and max query parameters.
"""
requested_page = int(request.REQUEST.get('page', 0))
requested_page_size = int(request.REQUEST.get('page_size', 50))
current_page = max(requested_page, 0)
start = current_page * requested_page_size

old_location = loc_mapper().translate_locator_to_location(location)
requested_sort = request.REQUEST.get('sort', 'date_added')
sort_direction = DESCENDING
if request.REQUEST.get('direction', 'asc').lower() == 'asc':
sort_direction = ASCENDING

# Convert the field name to the Mongo name
if requested_sort == 'date_added':
requested_sort = 'dateadded';
elif requested_sort == 'display_name':
requested_sort = 'displayname'

course_reference = StaticContent.compute_location(old_location.org, old_location.course, old_location.name)
if maxresults is not None:
maxresults = int(maxresults)
start = int(start) if start else 0
assets = contentstore().get_all_content_for_course(
course_reference, start=start, maxresults=maxresults,
sort=[('uploadDate', DESCENDING)]
)
else:
assets = contentstore().get_all_content_for_course(
course_reference, sort=[('uploadDate', DESCENDING)]
)
assets, total_count = contentstore().get_all_content_for_course(
course_reference, start=start, maxresults=requested_page_size, sort=[(requested_sort, sort_direction)]
)
end = start + len(assets)

asset_json = []
for asset in assets:
Expand All @@ -101,10 +126,13 @@ def _asset_index(request, 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,
'asset_list': json.dumps(asset_json),
'asset_callback_url': location.url_reverse('assets/', '')
return JsonResponse({
'start': start,
'end': end,
'page': current_page,
'pageSize': requested_page_size,
'totalCount': total_count,
'assets': asset_json
})


Expand Down
16 changes: 14 additions & 2 deletions cms/static/coffee/spec/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ requirejs.config({
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
"backbone.paginator": "xmodule_js/common_static/js/vendor/backbone.paginator.min",
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule",
Expand All @@ -38,6 +39,7 @@ requirejs.config({
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
"domReady": "xmodule_js/common_static/js/vendor/domReady",
"URI": "xmodule_js/common_static/js/vendor/URI.min",

"mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured",
"youtube": "//www.youtube.com/player_api?noext",
Expand Down Expand Up @@ -115,6 +117,10 @@ requirejs.config({
deps: ["backbone"],
exports: "Backbone.Associations"
},
"backbone.paginator": {
deps: ["backbone"],
exports: "Backbone.Paginator"
},
"youtube": {
exports: "YT"
},
Expand All @@ -139,6 +145,9 @@ requirejs.config({
]
MathJax.Hub.Configured()
},
"URI": {
exports: "URI"
},
"xmodule": {
exports: "XModule"
},
Expand Down Expand Up @@ -197,10 +206,13 @@ define([
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
"js/spec/transcripts/file_uploader_spec",

"js/spec/utils/module_spec",
"js/spec/models/explicit_url_spec"
"js/spec/views/baseview_spec",

"js/spec/utils/handle_iframe_binding_spec",
"js/spec/utils/module_spec",

"js/spec/views/baseview_spec",
"js/spec/views/paging_spec",

# these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js
Expand Down
Loading