Commit 978c19dd by Don Mitchell

Merge pull request #1325 from edx/dhm/paginate_assets

Add asset pagination
parents b0689a4e 214a3bd2
...@@ -6,19 +6,20 @@ Unit tests for the asset upload endpoint. ...@@ -6,19 +6,20 @@ Unit tests for the asset upload endpoint.
#pylint: disable=W0621 #pylint: disable=W0621
#pylint: disable=W0212 #pylint: disable=W0212
from datetime import datetime from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from pytz import UTC from pytz import UTC
import json
import re
from unittest import TestCase, skip from unittest import TestCase, skip
from .utils import CourseTestCase from .utils import CourseTestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from contentstore.views import assets from contentstore.views import assets
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
import json
class AssetsTestCase(CourseTestCase): class AssetsTestCase(CourseTestCase):
...@@ -147,3 +148,84 @@ class LockAssetTestCase(CourseTestCase): ...@@ -147,3 +148,84 @@ class LockAssetTestCase(CourseTestCase):
resp_asset = post_asset_update(False) resp_asset = post_asset_update(False)
self.assertFalse(resp_asset['locked']) self.assertFalse(resp_asset['locked'])
verify_asset_locked_state(False) 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)
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
)
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
asset_url = reverse(
'asset_index',
kwargs={
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name
}
)
resp = self.client.get(asset_url)
self.check_page_content(resp.content, 100)
# get first page of 10
resp = self.client.get(asset_url + "/max/10")
last_date = self.check_page_content(resp.content, 10)
# get next of 20
resp = self.client.get(asset_url + "/start/10/max/20")
last_date = self.check_page_content(resp.content, 20, last_date)
...@@ -23,6 +23,7 @@ from .access import get_location_and_verify_access ...@@ -23,6 +23,7 @@ from .access import get_location_and_verify_access
from util.json_request import JsonResponse from util.json_request import JsonResponse
import json import json
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from pymongo import DESCENDING
__all__ = ['asset_index', 'upload_asset'] __all__ = ['asset_index', 'upload_asset']
...@@ -30,11 +31,14 @@ __all__ = ['asset_index', 'upload_asset'] ...@@ -30,11 +31,14 @@ __all__ = ['asset_index', 'upload_asset']
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def asset_index(request, org, course, name): def asset_index(request, org, course, name, start=None, maxresults=None):
""" """
Display an editable asset library Display an editable asset library
org, course, name: Attributes of the Location for the item to edit org, course, name: Attributes of the Location for the item to edit
:param start: which index of the result list to start w/, used for paging results
:param maxresults: maximum results
""" """
location = get_location_and_verify_access(request, org, course, name) location = get_location_and_verify_access(request, org, course, name)
...@@ -47,10 +51,17 @@ def asset_index(request, org, course, name): ...@@ -47,10 +51,17 @@ def asset_index(request, org, course, name):
course_module = modulestore().get_item(location) course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name) course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference) if maxresults is not None:
maxresults = int(maxresults)
# sort in reverse upload date order start = int(start) if start else 0
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True) 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)]
)
asset_json = [] asset_json = []
for asset in assets: for asset in assets:
......
...@@ -71,7 +71,7 @@ urlpatterns = patterns('', # nopep8 ...@@ -71,7 +71,7 @@ urlpatterns = patterns('', # nopep8
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.edit_tabs', name='edit_tabs'), 'contentstore.views.edit_tabs', name='edit_tabs'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)(/start/(?P<start>\d+))?(/max/(?P<maxresults>\d+))?$',
'contentstore.views.asset_index', name='asset_index'), 'contentstore.views.asset_index', name='asset_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/(?P<asset_id>.+)?.*$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/(?P<asset_id>.+)?.*$',
'contentstore.views.assets.update_asset', name='update_asset'), 'contentstore.views.assets.update_asset', name='update_asset'),
......
...@@ -168,7 +168,7 @@ class ContentStore(object): ...@@ -168,7 +168,7 @@ class ContentStore(object):
def find(self, filename): def find(self, filename):
raise NotImplementedError raise NotImplementedError
def get_all_content_for_course(self, location): def get_all_content_for_course(self, location, start=0, maxresults=-1, sort=None):
''' '''
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example: Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
......
...@@ -130,10 +130,12 @@ class MongoContentStore(ContentStore): ...@@ -130,10 +130,12 @@ class MongoContentStore(ContentStore):
def get_all_content_thumbnails_for_course(self, location): def get_all_content_thumbnails_for_course(self, location):
return self._get_all_content_for_course(location, get_thumbnails=True) return self._get_all_content_for_course(location, get_thumbnails=True)
def get_all_content_for_course(self, location): def get_all_content_for_course(self, location, start=0, maxresults=-1, sort=None):
return self._get_all_content_for_course(location, get_thumbnails=False) return self._get_all_content_for_course(
location, start=start, maxresults=maxresults, get_thumbnails=False, sort=sort
)
def _get_all_content_for_course(self, location, get_thumbnails=False): def _get_all_content_for_course(self, location, get_thumbnails=False, start=0, maxresults=-1, sort=None):
''' '''
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example: Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
...@@ -156,7 +158,13 @@ class MongoContentStore(ContentStore): ...@@ -156,7 +158,13 @@ class MongoContentStore(ContentStore):
course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail", course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail",
course=location.course, org=location.org) course=location.course, org=location.org)
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation # 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
items = self.fs_files.find(location_to_query(course_filter)) if maxresults > 0:
items = self.fs_files.find(
location_to_query(course_filter),
skip=start, limit=maxresults, sort=sort
)
else:
items = self.fs_files.find(location_to_query(course_filter), sort=sort)
return list(items) return list(items)
def set_attr(self, location, attr, value=True): def set_attr(self, location, attr, value=True):
......
...@@ -106,24 +106,24 @@ def course_image_url(course): ...@@ -106,24 +106,24 @@ def course_image_url(course):
if course.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE: if course.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
return '/static/' + (course.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg" return '/static/' + (course.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg"
else: else:
loc = course.location._replace(tag='c4x', category='asset', name=course.course_image) loc = course.location.replace(tag='c4x', category='asset', name=course.course_image)
_path = StaticContent.get_url_path_from_location(loc) _path = StaticContent.get_url_path_from_location(loc)
return _path return _path
def find_file(fs, dirs, filename): def find_file(filesystem, dirs, filename):
""" """
Looks for a filename in a list of dirs on a filesystem, in the specified order. Looks for a filename in a list of dirs on a filesystem, in the specified order.
fs: an OSFS filesystem filesystem: an OSFS filesystem
dirs: a list of path objects dirs: a list of path objects
filename: a string filename: a string
Returns d / filename if found in dir d, else raises ResourceNotFoundError. Returns d / filename if found in dir d, else raises ResourceNotFoundError.
""" """
for d in dirs: for directory in dirs:
filepath = path(d) / filename filepath = path(directory) / filename
if fs.exists(filepath): if filesystem.exists(filepath):
return filepath return filepath
raise ResourceNotFoundError("Could not find {0}".format(filename)) raise ResourceNotFoundError("Could not find {0}".format(filename))
...@@ -167,7 +167,7 @@ def get_course_about_section(course, section_key): ...@@ -167,7 +167,7 @@ def get_course_about_section(course, section_key):
request = get_request_for_thread() request = get_request_for_thread()
loc = course.location._replace(category='about', name=section_key) loc = course.location.replace(category='about', name=section_key)
# Use an empty cache # Use an empty cache
field_data_cache = FieldDataCache([], course.id, request.user) field_data_cache = FieldDataCache([], course.id, request.user)
...@@ -255,13 +255,13 @@ def get_course_syllabus_section(course, section_key): ...@@ -255,13 +255,13 @@ def get_course_syllabus_section(course, section_key):
if section_key in ['syllabus', 'guest_syllabus']: if section_key in ['syllabus', 'guest_syllabus']:
try: try:
fs = course.system.resources_fs filesys = course.system.resources_fs
# first look for a run-specific version # first look for a run-specific version
dirs = [path("syllabus") / course.url_name, path("syllabus")] dirs = [path("syllabus") / course.url_name, path("syllabus")]
filepath = find_file(fs, dirs, section_key + ".html") filepath = find_file(filesys, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile: with filesys.open(filepath) as html_file:
return replace_static_urls( return replace_static_urls(
htmlFile.read().decode('utf-8'), html_file.read().decode('utf-8'),
getattr(course, 'data_dir', None), getattr(course, 'data_dir', None),
course_id=course.location.course_id, course_id=course.location.course_id,
static_asset_path=course.static_asset_path, static_asset_path=course.static_asset_path,
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
Tests for course access
"""
import mock import mock
from django.test import TestCase from django.test import TestCase
...@@ -44,13 +47,12 @@ class CoursesTest(TestCase): ...@@ -44,13 +47,12 @@ class CoursesTest(TestCase):
self.assertEqual("//{}/".format(CMS_BASE_TEST), get_cms_course_link_by_id("too/too/many/slashes")) self.assertEqual("//{}/".format(CMS_BASE_TEST), get_cms_course_link_by_id("too/too/many/slashes"))
self.assertEqual("//{}/org/num/course/name".format(CMS_BASE_TEST), get_cms_course_link_by_id('org/num/name')) self.assertEqual("//{}/org/num/course/name".format(CMS_BASE_TEST), get_cms_course_link_by_id('org/num/name'))
@mock.patch('xmodule.modulestore.django.get_current_request_hostname', mock.Mock(return_value='preview.localhost')) @mock.patch('xmodule.modulestore.django.get_current_request_hostname', mock.Mock(return_value='preview.localhost'))
@override_settings(HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={'preview\.': 'draft'}) @override_settings(HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={r'preview\.': 'draft'})
def test_default_modulestore_preview_mapping(self): def test_default_modulestore_preview_mapping(self):
self.assertEqual(get_default_store_name_for_current_request(), 'draft') self.assertEqual(get_default_store_name_for_current_request(), 'draft')
@mock.patch('xmodule.modulestore.django.get_current_request_hostname', mock.Mock(return_value='localhost')) @mock.patch('xmodule.modulestore.django.get_current_request_hostname', mock.Mock(return_value='localhost'))
@override_settings(HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={'preview\.': 'draft'}) @override_settings(HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS={r'preview\.': 'draft'})
def test_default_modulestore_published_mapping(self): def test_default_modulestore_published_mapping(self):
self.assertEqual(get_default_store_name_for_current_request(), 'default') self.assertEqual(get_default_store_name_for_current_request(), 'default')
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