diff --git a/common/djangoapps/entitlements/api/v1/views.py b/common/djangoapps/entitlements/api/v1/views.py index 8755dd702308115822a62c08c287375d05a83627..4d63c3c8eb0b52364e50d12ba228938f9c452a32 100644 --- a/common/djangoapps/entitlements/api/v1/views.py +++ b/common/djangoapps/entitlements/api/v1/views.py @@ -20,7 +20,7 @@ from entitlements.utils import is_course_run_entitlement_fullfillable from lms.djangoapps.commerce.utils import refund_entitlement from openedx.core.djangoapps.catalog.utils import get_course_runs_for_course from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf -from openedx.core.lib.api.paginators import DefaultPagination +from edx_rest_framework_extensions.paginators import DefaultPagination from student.models import CourseEnrollment from student.models import CourseEnrollmentException, AlreadyEnrolledError diff --git a/lms/djangoapps/ccx/api/v0/paginators.py b/lms/djangoapps/ccx/api/v0/paginators.py index 8f8730e0b80d315253a7d811fdde0bb523a87e4a..0bb72032f3a34f92366c0f6eb836b51f33bae310 100644 --- a/lms/djangoapps/ccx/api/v0/paginators.py +++ b/lms/djangoapps/ccx/api/v0/paginators.py @@ -1,6 +1,6 @@ """ CCX API v0 Paginators. """ -from openedx.core.lib.api.paginators import DefaultPagination +from edx_rest_framework_extensions.paginators import DefaultPagination class CCXAPIPagination(DefaultPagination): diff --git a/lms/djangoapps/course_api/views.py b/lms/djangoapps/course_api/views.py index 80e529c37f53e45313c983b0b2962076afc44f06..9eaad3fae5da05475876d01ee52d70a47bddcf98 100644 --- a/lms/djangoapps/course_api/views.py +++ b/lms/djangoapps/course_api/views.py @@ -5,7 +5,7 @@ Course API Views from django.core.exceptions import ValidationError from rest_framework.generics import ListAPIView, RetrieveAPIView -from openedx.core.lib.api.paginators import NamespacedPageNumberPagination +from edx_rest_framework_extensions.paginators import NamespacedPageNumberPagination from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes from .api import course_detail, list_courses diff --git a/lms/djangoapps/discussion_api/pagination.py b/lms/djangoapps/discussion_api/pagination.py index bf4c25ac5afccd4baf7050ce7c192b0d15a45904..fe13eaf1e614b3a64c67dd2b663f75279f27c475 100644 --- a/lms/djangoapps/discussion_api/pagination.py +++ b/lms/djangoapps/discussion_api/pagination.py @@ -3,7 +3,7 @@ Discussion API pagination support """ from rest_framework.utils.urls import replace_query_param -from openedx.core.lib.api.paginators import NamespacedPageNumberPagination +from edx_rest_framework_extensions.paginators import NamespacedPageNumberPagination class _Page(object): diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index a098ae985d580358c08d8a66a247231a7a892261..23700188cf925d9b0993c0b64d3794c4a970956c 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -24,7 +24,7 @@ from rest_framework_oauth.authentication import OAuth2Authentication from courseware.courses import get_course_with_access, has_access from django_comment_client.utils import has_discussion_privileges from lms.djangoapps.teams.models import CourseTeam, CourseTeamMembership -from openedx.core.lib.api.paginators import DefaultPagination, paginate_search_results +from edx_rest_framework_extensions.paginators import DefaultPagination, paginate_search_results from openedx.core.lib.api.parsers import MergePatchParser from openedx.core.lib.api.permissions import IsStaffOrReadOnly from openedx.core.lib.api.view_utils import ( diff --git a/lms/envs/common.py b/lms/envs/common.py index bfc3191af7ac3982ca9ce22287e7559057ddc604..2b4d265264eb8c99a6e0f49e7a38b627e769f69c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2373,7 +2373,7 @@ CSRF_COOKIE_SECURE = False ######################### Django Rest Framework ######################## REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'openedx.core.lib.api.paginators.DefaultPagination', + 'DEFAULT_PAGINATION_CLASS': 'edx_rest_framework_extensions.paginators.DefaultPagination', 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', ), diff --git a/openedx/core/djangoapps/bookmarks/views.py b/openedx/core/djangoapps/bookmarks/views.py index d1dc1af009a5c07ea6ee3cc93928feac056b0455..a2794b40ed5e4886c10ade8349ac5616788b1d43 100644 --- a/openedx/core/djangoapps/bookmarks/views.py +++ b/openedx/core/djangoapps/bookmarks/views.py @@ -21,7 +21,7 @@ from rest_framework_oauth.authentication import OAuth2Authentication import eventtracking from openedx.core.djangoapps.bookmarks.api import BookmarksLimitReachedError -from openedx.core.lib.api.paginators import DefaultPagination +from edx_rest_framework_extensions.paginators import DefaultPagination from openedx.core.lib.api.permissions import IsUserInUrl from openedx.core.lib.url_utils import unquote_slashes from xmodule.modulestore.exceptions import ItemNotFoundError diff --git a/openedx/core/lib/api/paginators.py b/openedx/core/lib/api/paginators.py deleted file mode 100644 index e94cfe9d5d2f31985650d0d010529740ca7e4906..0000000000000000000000000000000000000000 --- a/openedx/core/lib/api/paginators.py +++ /dev/null @@ -1,124 +0,0 @@ -""" Paginatator methods for edX API implementations.""" - -from django.core.paginator import InvalidPage, Paginator -from django.http import Http404 -from rest_framework import pagination -from rest_framework.response import Response - - -class DefaultPagination(pagination.PageNumberPagination): - """ - Default paginator for APIs in edx-platform. - - This is configured in settings to be automatically used - by any subclass of Django Rest Framework's generic API views. - """ - page_size_query_param = "page_size" - max_page_size = 100 - - def get_paginated_response(self, data): - """ - Annotate the response with pagination information. - """ - return Response({ - 'next': self.get_next_link(), - 'previous': self.get_previous_link(), - 'count': self.page.paginator.count, - 'num_pages': self.page.paginator.num_pages, - 'current_page': self.page.number, - 'start': (self.page.number - 1) * self.get_page_size(self.request), - 'results': data - }) - - -class NamespacedPageNumberPagination(pagination.PageNumberPagination): - """ - Pagination scheme that returns results with pagination metadata - embedded in a "pagination" attribute. Can be used with data - that comes as a list of items, or as a dict with a "results" - attribute that contains a list of items. - """ - - page_size_query_param = "page_size" - - def get_result_count(self): - """ - Returns total number of results - """ - return self.page.paginator.count - - def get_num_pages(self): - """ - Returns total number of pages the results are divided into - """ - return self.page.paginator.num_pages - - def get_paginated_response(self, data): - """ - Annotate the response with pagination information - """ - metadata = { - 'next': self.get_next_link(), - 'previous': self.get_previous_link(), - 'count': self.get_result_count(), - 'num_pages': self.get_num_pages(), - } - if isinstance(data, dict): - if 'results' not in data: - raise TypeError(u'Malformed result dict') - data['pagination'] = metadata - else: - data = { - 'results': data, - 'pagination': metadata, - } - return Response(data) - - -def paginate_search_results(object_class, search_results, page_size, page): - """ - Takes edx-search results and returns a Page object populated - with db objects for that page. - - :param object_class: Model class to use when querying the db for objects. - :param search_results: edX-search results. - :param page_size: Number of results per page. - :param page: Page number. - :return: Paginator object with model objects - """ - paginator = Paginator(search_results['results'], page_size) - - # This code is taken from within the GenericAPIView#paginate_queryset method. - # It is common code, but - try: - page_number = paginator.validate_number(page) - except InvalidPage: - if page == 'last': - page_number = paginator.num_pages - else: - raise Http404("Page is not 'last', nor can it be converted to an int.") - - try: - paged_results = paginator.page(page_number) - except InvalidPage as exception: - raise Http404( - "Invalid page {page_number}: {message}".format( - page_number=page_number, - message=str(exception) - ) - ) - - search_queryset_pks = [item['data']['pk'] for item in paged_results.object_list] - queryset = object_class.objects.filter(pk__in=search_queryset_pks) - - def ordered_objects(primary_key): - """ Returns database object matching the search result object""" - for obj in queryset: - if obj.pk == primary_key: - return obj - - # map over the search results and get a list of database objects in the same order - object_results = map(ordered_objects, search_queryset_pks) - paged_results.object_list = object_results - - return paged_results diff --git a/openedx/core/lib/api/tests/test_paginators.py b/openedx/core/lib/api/tests/test_paginators.py deleted file mode 100644 index 0111c9ef205ba63b561403fbaf542a050d57e61a..0000000000000000000000000000000000000000 --- a/openedx/core/lib/api/tests/test_paginators.py +++ /dev/null @@ -1,190 +0,0 @@ -""" Tests paginator methods """ - -from collections import namedtuple - -import ddt -from mock import Mock, MagicMock -from nose.plugins.attrib import attr -from unittest import TestCase -from django.http import Http404 -from django.test import RequestFactory -from rest_framework import serializers - -from openedx.core.lib.api.paginators import NamespacedPageNumberPagination, paginate_search_results - - -@attr(shard=2) -@ddt.ddt -class PaginateSearchResultsTestCase(TestCase): - """Test cases for paginate_search_results method""" - - def setUp(self): - super(PaginateSearchResultsTestCase, self).setUp() - - self.default_size = 6 - self.default_page = 1 - self.search_results = { - "count": 3, - "took": 1, - "results": [ - { - '_id': 0, - 'data': { - 'pk': 0, - 'name': 'object 0' - } - }, - { - '_id': 1, - 'data': { - 'pk': 1, - 'name': 'object 1' - } - }, - { - '_id': 2, - 'data': { - 'pk': 2, - 'name': 'object 2' - } - }, - { - '_id': 3, - 'data': { - 'pk': 3, - 'name': 'object 3' - } - }, - { - '_id': 4, - 'data': { - 'pk': 4, - 'name': 'object 4' - } - }, - { - '_id': 5, - 'data': { - 'pk': 5, - 'name': 'object 5' - } - }, - ] - } - self.mock_model = Mock() - self.mock_model.objects = Mock() - self.mock_model.objects.filter = Mock() - - @ddt.data( - (1, 1, True), - (1, 3, True), - (1, 5, True), - (1, 10, False), - (2, 1, True), - (2, 3, False), - (2, 5, False), - ) - @ddt.unpack - def test_paginated_results(self, page_number, page_size, has_next): - """ Test the page returned has the expected db objects and acts - like a proper page object. - """ - id_range = get_object_range(page_number, page_size) - db_objects = [build_mock_object(obj_id) for obj_id in id_range] - self.mock_model.objects.filter = MagicMock(return_value=db_objects) - - page = paginate_search_results(self.mock_model, self.search_results, page_size, page_number) - - self.mock_model.objects.filter.assert_called_with(pk__in=id_range) - self.assertEquals(db_objects, page.object_list) - self.assertTrue(page.number, page_number) - self.assertEquals(page.has_next(), has_next) - - def test_paginated_results_last_keyword(self): - """ Test the page returned has the expected db objects and acts - like a proper page object using 'last' keyword. - """ - page_number = 2 - page_size = 3 - id_range = get_object_range(page_number, page_size) - db_objects = [build_mock_object(obj_id) for obj_id in id_range] - self.mock_model.objects.filter = MagicMock(return_value=db_objects) - page = paginate_search_results(self.mock_model, self.search_results, page_size, 'last') - - self.mock_model.objects.filter.assert_called_with(pk__in=id_range) - self.assertEquals(db_objects, page.object_list) - self.assertTrue(page.number, page_number) - self.assertFalse(page.has_next()) - - @ddt.data(10, -1, 0, 'str') - def test_invalid_page_number(self, page_num): - """ Test that a Http404 error is raised with non-integer and out-of-range pages - """ - with self.assertRaises(Http404): - paginate_search_results(self.mock_model, self.search_results, self.default_size, page_num) - - -@attr(shard=2) -class NamespacedPaginationTestCase(TestCase): - """ - Test behavior of `NamespacedPageNumberPagination` - """ - - TestUser = namedtuple('TestUser', ['username', 'email']) - - class TestUserSerializer(serializers.Serializer): # pylint: disable=abstract-method - """ - Simple serializer to paginate results from - """ - username = serializers.CharField() - email = serializers.CharField() - - expected_data = { - 'results': [ - {'username': 'user_5', 'email': 'user_5@example.com'}, - {'username': 'user_6', 'email': 'user_6@example.com'}, - {'username': 'user_7', 'email': 'user_7@example.com'}, - {'username': 'user_8', 'email': 'user_8@example.com'}, - {'username': 'user_9', 'email': 'user_9@example.com'}, - ], - 'pagination': { - 'next': 'http://testserver/endpoint?page=3&page_size=5', - 'previous': 'http://testserver/endpoint?page_size=5', - 'count': 25, - 'num_pages': 5, - } - } - - def setUp(self): - super(NamespacedPaginationTestCase, self).setUp() - self.paginator = NamespacedPageNumberPagination() - self.users = [self.TestUser('user_{}'.format(idx), 'user_{}@example.com'.format(idx)) for idx in xrange(25)] - self.request_factory = RequestFactory() - - def test_basic_pagination(self): - request = self.request_factory.get('/endpoint', data={'page': 2, 'page_size': 5}) - request.query_params = {'page': 2, 'page_size': 5} - paged_users = self.paginator.paginate_queryset(self.users, request) - results = self.TestUserSerializer(paged_users, many=True).data - self.assertEqual(self.expected_data, self.paginator.get_paginated_response(results).data) - - -def build_mock_object(obj_id): - """ Build a mock object with the passed id""" - mock_object = Mock() - object_config = { - 'pk': obj_id, - 'name': "object {}".format(obj_id) - } - mock_object.configure_mock(**object_config) - return mock_object - - -def get_object_range(page, page_size): - """ Get the range of expected object ids given a page and page size. - This will take into account the max_id of the sample data. Currently 5. - """ - max_id = 5 - start = min((page - 1) * page_size, max_id) - end = min(start + page_size, max_id + 1) - return range(start, end)