diff --git a/component_catalog/tests/test_views.py b/component_catalog/tests/test_views.py index a2703e40..5efafcb1 100644 --- a/component_catalog/tests/test_views.py +++ b/component_catalog/tests/test_views.py @@ -42,6 +42,7 @@ from component_catalog.tests import make_package from component_catalog.views import ComponentAddView from component_catalog.views import ComponentListView +from component_catalog.views import PackageListView from component_catalog.views import PackageTabScanView from dejacode_toolkit.scancodeio import get_webhook_url from dejacode_toolkit.vulnerablecode import VulnerableCode @@ -55,6 +56,7 @@ from dje.tests import add_perms from dje.tests import create_superuser from dje.tests import create_user +from dje.views import PaginationMixin from license_library.models import License from license_library.models import LicenseAssignedTag from license_library.models import LicenseTag @@ -517,8 +519,8 @@ def test_component_catalog_history_tab(self): self.assertContains(response, "Changed name.") def test_component_catalog_productcomponent_secured_hierarchy_and_product_usage(self): - component1 = Component.objects.create(name="c1", dataspace=self.nexb_dataspace) - product1 = Product.objects.create(name="p1", dataspace=self.nexb_dataspace) + component1 = Component.objects.create(name="component1", dataspace=self.nexb_dataspace) + product1 = Product.objects.create(name="product1", dataspace=self.nexb_dataspace) ProductComponent.objects.create( product=product1, component=component1, dataspace=self.nexb_dataspace ) @@ -1134,6 +1136,20 @@ def test_package_list_view_num_queries(self): with self.assertNumQueries(16): self.client.get(reverse("component_catalog:package_list")) + def test_package_list_view_pagination(self): + list_view = PackageListView() + + # Default value from the PaginationMixin.default_paginate_by + with override_settings(DEJACODE_PAGINATE_BY={}): + self.assertIsNone(list_view.paginate_by) + expected = PaginationMixin.default_paginate_by + self.assertEqual(expected, list_view.get_paginate_by(queryset=None)) + + # Value from custom DEJACODE_PAGINATE_BY + with override_settings(DEJACODE_PAGINATE_BY={"package": 20}): + self.assertIsNone(list_view.paginate_by) + self.assertEqual(20, list_view.get_paginate_by(queryset=None)) + def test_package_views_urls(self): p1 = Package( filename="filename.zip", diff --git a/component_catalog/views.py b/component_catalog/views.py index dd4d9848..a650e000 100644 --- a/component_catalog/views.py +++ b/component_catalog/views.py @@ -353,7 +353,6 @@ class ComponentListView( template_list_table = "component_catalog/tables/component_list_table.html" include_reference_dataspace = True put_results_in_session = True - paginate_by = settings.PAGINATE_BY or 200 group_name_version = True table_headers = ( @@ -1734,9 +1733,9 @@ class ScanListView( AddPackagePermissionMixin, APIWrapperListView, ): - paginate_by = 50 template_name = "component_catalog/scan_list.html" template_list_table = "component_catalog/tables/scan_list_table.html" + paginate_by = settings.DEJACODE_PAGINATE_BY.get("scan", 50) def dispatch(self, request, *args, **kwargs): user = self.request.user diff --git a/dejacode/settings.py b/dejacode/settings.py index eff683f8..fd0941fc 100644 --- a/dejacode/settings.py +++ b/dejacode/settings.py @@ -10,6 +10,7 @@ import sys import tempfile +import warnings from pathlib import Path import environ @@ -383,9 +384,31 @@ def gettext_noop(s): # Set False to hide the "Product Portfolio" section in the navbar. SHOW_PP_IN_NAV = env.bool("SHOW_PP_IN_NAV", default=True) +# An integer specifying how many objects should be displayed per table whithin tabs. +TAB_PAGINATE_BY = env.int("TAB_PAGINATE_BY", default=100) + # An integer specifying how many objects should be displayed per page. PAGINATE_BY = env.int("PAGINATE_BY", default=None) -TAB_PAGINATE_BY = env.int("TAB_PAGINATE_BY", default=100) +if PAGINATE_BY: + warnings.warn("The PAGINATE_BY setting is deprecated. Use DEJACODE_PAGINATE_BY instead.") + + +# List views pagination, controls the number of items displayed per page. +# Syntax in .env: DEJACODE_PAGINATE_BY=product=20,package=100,license=100,request=50,scan=50 +DEJACODE_PAGINATE_BY = env.dict( + "DEJACODE_PAGINATE_BY", + default={ + "product": 50, + "component": 100, + "package": 100, + "license": 100, + "owner": 100, + "report": 50, + "request": 50, + "scan": 50, + "vulnerability": 100, + }, +) ADMIN_FORMS_CONFIGURATION = env.dict("ADMIN_FORMS_CONFIGURATION", default={}) diff --git a/dje/views.py b/dje/views.py index ee73845d..5fe4ec3f 100644 --- a/dje/views.py +++ b/dje/views.py @@ -260,8 +260,32 @@ def get_queryset(self): return qs.scope(dataspace, include_reference=self.include_reference_dataspace) -class PreviousNextPaginationMixin: +class PaginationMixin: query_dict_page_param = "page" + paginate_by = None + default_paginate_by = 100 + + def get_paginate_by(self, queryset): + """ + Determine the number of items per page. + + Resolution order: + 1. ``paginate_by`` set directly on the view instance. + 2. Per-model value from the ``DEJACODE_PAGINATE_BY`` setting. + 3. ``default_paginate_by`` as a fallback. + """ + if self.paginate_by: + return self.paginate_by + + if self.model and settings.DEJACODE_PAGINATE_BY: + model_name = self.model._meta.model_name + if paginate_by := settings.DEJACODE_PAGINATE_BY.get(model_name): + try: + return int(paginate_by) + except ValueError: + return self.default_paginate_by + + return self.default_paginate_by def get_previous_next(self, page_obj): """Return url links for the previous and next navigation.""" @@ -330,12 +354,11 @@ class DataspacedFilterView( GetDataspaceMixin, HasPermissionMixin, TableHeaderMixin, - PreviousNextPaginationMixin, + PaginationMixin, FilterView, ): template_name = "object_list_base.html" template_list_table = None - paginate_by = settings.PAGINATE_BY or 100 # Required if `show_previous_and_next_object_links` enabled on the # details view. put_results_in_session = False @@ -2246,7 +2269,7 @@ def page(self, number): class APIWrapperListView( - PreviousNextPaginationMixin, + PaginationMixin, ListView, ): paginate_by = 100 diff --git a/docs/application-settings.rst b/docs/application-settings.rst index 4e638c56..d7e1903f 100644 --- a/docs/application-settings.rst +++ b/docs/application-settings.rst @@ -162,6 +162,16 @@ longer than this value. # 1 hour, in seconds. SESSION_COOKIE_AGE=3600 +.. _dejacode_settings_paginate_by: + +DEJACODE_PAGINATE_BY +-------------------- + +The number of objects display per page for each object type can be customized with the +following setting:: + + DEJACODE_PAGINATE_BY=product=20,package=100,license=100,report=50,request=50,scan=50 + DEJACODE_LOG_LEVEL ------------------ diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 3584bce1..01403eb2 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -99,7 +99,7 @@ from dje.views import Header from dje.views import LicenseDataForBuilderMixin from dje.views import ObjectDetailsView -from dje.views import PreviousNextPaginationMixin +from dje.views import PaginationMixin from dje.views import SendAboutFilesView from dje.views import TabContentView from dje.views import TabField @@ -164,7 +164,6 @@ class ProductListView( filterset_class = ProductFilterSet template_name = "product_portfolio/product_list.html" template_list_table = "product_portfolio/tables/product_list_table.html" - paginate_by = 50 put_results_in_session = False group_name_version = True table_headers = ( @@ -733,7 +732,7 @@ def get_context_data(self, **kwargs): class ProductTabInventoryView( LoginRequiredMixin, BaseProductViewMixin, - PreviousNextPaginationMixin, + PaginationMixin, TabContentView, ): template_name = "product_portfolio/tabs/tab_inventory.html" @@ -978,7 +977,7 @@ def inject_scan_data(scancodeio, feature_grouped, dataspace_uuid): class ProductTabCodebaseView( LoginRequiredMixin, BaseProductViewMixin, - PreviousNextPaginationMixin, + PaginationMixin, TabContentView, ): template_name = "product_portfolio/tabs/tab_codebase.html" @@ -1059,7 +1058,7 @@ def has_any_values(field_name): class ProductTabDependenciesView( LoginRequiredMixin, BaseProductViewMixin, - PreviousNextPaginationMixin, + PaginationMixin, TableHeaderMixin, TabContentView, ): @@ -1138,7 +1137,7 @@ def get_context_data(self, **kwargs): class ProductTabVulnerabilitiesView( LoginRequiredMixin, BaseProductViewMixin, - PreviousNextPaginationMixin, + PaginationMixin, TableHeaderMixin, TabContentView, ): diff --git a/reporting/views.py b/reporting/views.py index 86aad47b..ac4c49d9 100644 --- a/reporting/views.py +++ b/reporting/views.py @@ -34,7 +34,7 @@ from dje.views import DataspacedFilterView from dje.views import DownloadableMixin from dje.views import HasPermissionMixin -from dje.views import PreviousNextPaginationMixin +from dje.views import PaginationMixin from reporting.filters import ReportFilterSet from reporting.forms import RuntimeFilterBaseFormSet from reporting.forms import RuntimeFilterForm @@ -81,7 +81,7 @@ def get(self, request, *args, **kwargs): class ReportDetailsView( LoginRequiredMixin, - PreviousNextPaginationMixin, + PaginationMixin, BootstrapCSSMixin, DownloadableMixin, HasPermissionMixin, @@ -294,7 +294,6 @@ class ReportListView( filterset_class = ReportFilterSet template_name = "reporting/report_list.html" template_list_table = "reporting/includes/report_list_table.html" - paginate_by = 50 def get_queryset(self): qs = super().get_queryset() diff --git a/workflow/views.py b/workflow/views.py index bb12cff1..ecb13e8d 100644 --- a/workflow/views.py +++ b/workflow/views.py @@ -45,7 +45,6 @@ class RequestListView( filterset_class = RequestFilterSet template_name = "workflow/request_list.html" template_list_table = "workflow/includes/request_list_table.html" - paginate_by = 50 def get_queryset(self): """