From 3042eef0103164c57cab64bca22f0f67b31f6632 Mon Sep 17 00:00:00 2001 From: Nathan Levesque Date: Wed, 25 Mar 2026 17:25:21 -0400 Subject: [PATCH 1/3] Update DRF pagination to only use limited fields --- articles/views.py | 13 ------------- channels/views.py | 4 ++-- learning_resources/views.py | 26 ++++---------------------- learning_resources_search/views.py | 1 + main/pagination.py | 27 +++++++++++++++++++++++++++ main/settings.py | 1 + news_events/views.py | 13 ------------- profiles/views.py | 2 +- video_shorts/views.py | 5 ++--- 9 files changed, 38 insertions(+), 54 deletions(-) create mode 100644 main/pagination.py diff --git a/articles/views.py b/articles/views.py index 73d7ed831e..cd6d971327 100644 --- a/articles/views.py +++ b/articles/views.py @@ -9,7 +9,6 @@ ) from rest_framework import status, viewsets from rest_framework.decorators import action -from rest_framework.pagination import LimitOffsetPagination from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -24,17 +23,6 @@ from .permissions import CanEditArticle, CanViewArticle, is_article_group_user from .serializers import ArticleImageUploadSerializer -# Create your views here. - - -class DefaultPagination(LimitOffsetPagination): - """ - Pagination class for learning_resources viewsets which gets default_limit and max_limit from settings - """ # noqa: E501 - - default_limit = 10 - max_limit = 100 - @extend_schema_view( list=extend_schema( @@ -66,7 +54,6 @@ class ArticleViewSet(viewsets.ModelViewSet): serializer_class = RichTextArticleSerializer queryset = Article.objects.all() - pagination_class = DefaultPagination permission_classes = [CanViewArticle, CanEditArticle] http_method_names = VALID_HTTP_METHODS diff --git a/channels/views.py b/channels/views.py index 419f3b471c..021a9e07f1 100644 --- a/channels/views.py +++ b/channels/views.py @@ -25,7 +25,6 @@ ChannelSerializer, ChannelWriteSerializer, ) -from learning_resources.views import DefaultPagination from main.constants import VALID_HTTP_METHODS from main.permissions import AnonymousAccessReadonlyPermission from main.utils import cache_page_for_all_users @@ -68,7 +67,6 @@ class ChannelViewSet( or organizations at MIT and are a high-level categorization of content. """ - pagination_class = DefaultPagination permission_classes = (HasChannelPermission,) http_method_names = VALID_HTTP_METHODS lookup_field = "id" @@ -164,6 +162,7 @@ class ChannelModeratorListView(ListCreateAPIView): permission_classes = (ChannelModeratorPermissions,) serializer_class = ChannelModeratorSerializer + pagination_class = None def get_queryset(self): """ @@ -211,6 +210,7 @@ class ChannelCountsView(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = ChannelCountsSerializer permission_classes = (AnonymousAccessReadonlyPermission,) + pagination_class = None def get_queryset(self): """ diff --git a/learning_resources/views.py b/learning_resources/views.py index 235862d706..95afd938a1 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -23,7 +23,6 @@ from rest_framework.decorators import action from rest_framework.filters import OrderingFilter from rest_framework.generics import get_object_or_404 -from rest_framework.pagination import LimitOffsetPagination from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework_nested.viewsets import NestedViewSetMixin @@ -104,6 +103,7 @@ ) from main.constants import VALID_HTTP_METHODS from main.filters import MultipleOptionsFilterBackend +from main.pagination import LargePagination from main.permissions import ( AnonymousAccessReadonlyPermission, is_admin_user, @@ -130,22 +130,6 @@ def show_content_file_content(user): log = logging.getLogger(__name__) -class DefaultPagination(LimitOffsetPagination): - """ - Pagination class for learning_resources viewsets which gets default_limit and max_limit from settings - """ # noqa: E501 - - default_limit = 10 - max_limit = 100 - - -class LargePagination(DefaultPagination): - """Large pagination for small resources, e.g., topics.""" - - default_limit = 1000 - max_limit = 1000 - - @extend_schema_view( list=extend_schema( summary="List", @@ -162,7 +146,6 @@ class BaseLearningResourceViewSet(viewsets.ReadOnlyModelViewSet): """ permission_classes = (AnonymousAccessReadonlyPermission,) - pagination_class = DefaultPagination filter_backends = [MultipleOptionsFilterBackend] filterset_class = LearningResourceFilter lookup_field = "id" @@ -507,6 +490,7 @@ class LearningPathMembershipViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = MicroLearningPathRelationshipSerializer permission_classes = (permissions.HasLearningPathMembershipPermissions,) + pagination_class = None http_method_names = ["get"] def get_queryset(self): @@ -552,7 +536,6 @@ class ResourceListItemsViewSet(NestedViewSetMixin, viewsets.ReadOnlyModelViewSet parent_lookup_kwargs = {"learning_resource_id": "parent_id"} permission_classes = (AnonymousAccessReadonlyPermission,) serializer_class = LearningResourceRelationshipSerializer - pagination_class = DefaultPagination queryset = ( LearningResourceRelationship.objects.select_related("child") .prefetch_related( @@ -603,6 +586,7 @@ class LearningResourceListRelationshipViewSet(viewsets.GenericViewSet): """ permission_classes = (AnonymousAccessReadonlyPermission,) + pagination_class = None filter_backends = [MultipleOptionsFilterBackend] queryset = LearningResourceRelationship.objects.select_related("parent", "child") http_method_names = ["patch"] @@ -861,7 +845,6 @@ class ContentFileViewSet(viewsets.ReadOnlyModelViewSet): .filter(published=True) .order_by("-created_on") ) - pagination_class = DefaultPagination filter_backends = [MultipleOptionsFilterBackend] filterset_class = ContentFileFilter private_fields = ["content"] @@ -912,7 +895,6 @@ class UserListViewSet(viewsets.ModelViewSet): """ serializer_class = UserListSerializer - pagination_class = DefaultPagination permission_classes = (HasUserListPermissions,) http_method_names = VALID_HTTP_METHODS lookup_url_kwarg = "id" @@ -974,7 +956,6 @@ class UserListItemViewSet(NestedViewSetMixin, viewsets.ModelViewSet): "position" ) serializer_class = UserListRelationshipSerializer - pagination_class = DefaultPagination permission_classes = (HasUserListItemPermissions,) http_method_names = VALID_HTTP_METHODS parent_lookup_kwargs = {"userlist_id": "parent"} @@ -1020,6 +1001,7 @@ class UserListMembershipViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = MicroUserListRelationshipSerializer permission_classes = (IsAuthenticated,) + pagination_class = None http_method_names = ["get"] def get_queryset(self): diff --git a/learning_resources_search/views.py b/learning_resources_search/views.py index 50a83fbdd6..99f1dd932e 100644 --- a/learning_resources_search/views.py +++ b/learning_resources_search/views.py @@ -111,6 +111,7 @@ class UserSearchSubscriptionViewSet(mixins.ListModelMixin, viewsets.GenericViewS permission_classes = (IsAuthenticated,) serializer_class = PercolateQuerySerializer + pagination_class = None http_method_names = ["get", "post", "delete"] def get_queryset(self): diff --git a/main/pagination.py b/main/pagination.py new file mode 100644 index 0000000000..ed86ae1181 --- /dev/null +++ b/main/pagination.py @@ -0,0 +1,27 @@ +from rest_framework.pagination import LimitOffsetPagination + + +class DefaultPagination(LimitOffsetPagination): + """ + Default pagination class for rest APIs + """ + + count_fields = ("pk",) + + default_limit = 10 + max_limit = 100 + + def get_count(self, queryset): + """Get the count of objects in the queryset""" + # we additionally filter this down to a subset of fields + try: + return queryset.only(*self.count_fields).count() + except (AttributeError, TypeError): + return len(queryset) + + +class LargePagination(DefaultPagination): + """Large pagination for small resources, e.g., topics.""" + + default_limit = 1000 + max_limit = 1000 diff --git a/main/settings.py b/main/settings.py index c58f835ed7..98305dc2f8 100644 --- a/main/settings.py +++ b/main/settings.py @@ -664,6 +664,7 @@ def get_all_config_keys(): "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework.authentication.SessionAuthentication", ), + "DEFAULT_PAGINATION_CLASS": "main.pagination.DefaultPagination", "EXCEPTION_HANDLER": "main.exceptions.api_exception_handler", "TEST_REQUEST_DEFAULT_FORMAT": "json", "TEST_REQUEST_RENDERER_CLASSES": [ diff --git a/news_events/views.py b/news_events/views.py index c7bdba18ca..18864709c0 100644 --- a/news_events/views.py +++ b/news_events/views.py @@ -5,7 +5,6 @@ from django.utils.decorators import method_decorator from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import viewsets -from rest_framework.pagination import LimitOffsetPagination from main.filters import MultipleOptionsFilterBackend from main.permissions import AnonymousAccessReadonlyPermission @@ -16,16 +15,6 @@ from news_events.serializers import FeedItemSerializer, FeedSourceSerializer -class DefaultPagination(LimitOffsetPagination): - """ - Pagination class for news/events viewsets which gets - default_limit and max_limit from settings - """ - - default_limit = 10 - max_limit = 100 - - @extend_schema_view( list=extend_schema( description="Get a paginated list of feed items.", @@ -42,7 +31,6 @@ class FeedItemViewSet(viewsets.ReadOnlyModelViewSet): resource_type_name_plural = "News and Events" serializer_class = FeedItemSerializer permission_classes = (AnonymousAccessReadonlyPermission,) - pagination_class = DefaultPagination filter_backends = [MultipleOptionsFilterBackend] filterset_class = FeedItemFilter queryset = ( @@ -80,7 +68,6 @@ class FeedSourceViewSet(viewsets.ReadOnlyModelViewSet): """ permission_classes = (AnonymousAccessReadonlyPermission,) - pagination_class = DefaultPagination resource_type_name_plural = "News & Events Sources" serializer_class = FeedSourceSerializer filter_backends = [MultipleOptionsFilterBackend] diff --git a/profiles/views.py b/profiles/views.py index 6e92dceb97..2c9d30d369 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -37,7 +37,7 @@ class UserViewSet(viewsets.ModelViewSet): """View for users""" permission_classes = (IsAuthenticated, IsStaffPermission) - + pagination_class = None serializer_class = UserSerializer queryset = get_user_model().objects.filter(is_active=True) diff --git a/video_shorts/views.py b/video_shorts/views.py index fa711fb7ee..d8e03bf12b 100644 --- a/video_shorts/views.py +++ b/video_shorts/views.py @@ -4,21 +4,20 @@ from django.utils.decorators import method_decorator from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import viewsets -from rest_framework.pagination import LimitOffsetPagination +from main.pagination import DefaultPagination from main.permissions import AnonymousAccessReadonlyPermission from main.utils import cache_page_for_all_users from video_shorts.models import VideoShort from video_shorts.serializers import VideoShortSerializer -class VideoShortPagination(LimitOffsetPagination): +class VideoShortPagination(DefaultPagination): """ Pagination class for video shorts viewset with a default limit of 12 """ default_limit = 12 - max_limit = 100 @extend_schema_view( From c84beab29b60a695f2539a6cdb33904f3529ad56 Mon Sep 17 00:00:00 2001 From: Nathan Levesque Date: Thu, 26 Mar 2026 13:14:38 -0400 Subject: [PATCH 2/3] Fix the summary endpoint --- learning_resources/views.py | 4 +++- main/pagination.py | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/learning_resources/views.py b/learning_resources/views.py index 95afd938a1..0e9db69575 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -317,7 +317,9 @@ def summary(self, request, **kwargs): # noqa: ARG002 Intended to be performant with large page sizes. """ queryset = self.filter_queryset( - self.get_queryset().values("id", "last_modified") + # we don't use `self.get_queryset()` here because there are incomplatible + # `select_related()` invocations and we don't need related data anyway + LearningResource.objects.filter(published=True).only("id", "last_modified") ) page = self.paginate_queryset(queryset) diff --git a/main/pagination.py b/main/pagination.py index ed86ae1181..fc4d3cec62 100644 --- a/main/pagination.py +++ b/main/pagination.py @@ -14,10 +14,7 @@ class DefaultPagination(LimitOffsetPagination): def get_count(self, queryset): """Get the count of objects in the queryset""" # we additionally filter this down to a subset of fields - try: - return queryset.only(*self.count_fields).count() - except (AttributeError, TypeError): - return len(queryset) + return queryset.only(*self.count_fields).count() class LargePagination(DefaultPagination): From 03f664d8c056ec62fc5f6f0c8cf8b09dffbc9302 Mon Sep 17 00:00:00 2001 From: Nathan Levesque Date: Mon, 30 Mar 2026 10:26:54 -0400 Subject: [PATCH 3/3] Feedback --- learning_resources/views.py | 4 +++- testimonials/views.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/learning_resources/views.py b/learning_resources/views.py index 0e9db69575..195618d01b 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -319,7 +319,9 @@ def summary(self, request, **kwargs): # noqa: ARG002 queryset = self.filter_queryset( # we don't use `self.get_queryset()` here because there are incomplatible # `select_related()` invocations and we don't need related data anyway - LearningResource.objects.filter(published=True).only("id", "last_modified") + LearningResource.objects.filter(published=True) + .only("id", "last_modified") + .distinct() ) page = self.paginate_queryset(queryset) diff --git a/testimonials/views.py b/testimonials/views.py index 10472e3051..88836f07ca 100644 --- a/testimonials/views.py +++ b/testimonials/views.py @@ -4,8 +4,8 @@ from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework.viewsets import ReadOnlyModelViewSet -from learning_resources.views import LargePagination from main.filters import MultipleOptionsFilterBackend +from main.pagination import LargePagination from main.permissions import AnonymousAccessReadonlyPermission from main.utils import now_in_utc from testimonials.filters import AttestationFilter