Add PAGINATE_COUNT, MAX_PAGE_SIZE

This commit is contained in:
jeremystretch 2021-10-26 11:39:39 -04:00
parent 94804fecd8
commit 64d8512fc3
9 changed files with 62 additions and 58 deletions

View File

@ -25,9 +25,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
# ('Logging', { # ('Logging', {
# 'fields': ('CHANGELOG_RETENTION',), # 'fields': ('CHANGELOG_RETENTION',),
# }), # }),
# ('Pagination', { ('Pagination', {
# 'fields': ('MAX_PAGE_SIZE', 'PAGINATE_COUNT'), 'fields': ('PAGINATE_COUNT', 'MAX_PAGE_SIZE'),
# }), }),
('Miscellaneous', { ('Miscellaneous', {
'fields': ('MAINTENANCE_MODE', 'MAPS_URL'), 'fields': ('MAINTENANCE_MODE', 'MAPS_URL'),
}), }),

View File

@ -9,6 +9,7 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from ipam.models import * from ipam.models import *
from netbox.config import Config
from utilities.constants import ADVISORY_LOCK_KEYS from utilities.constants import ADVISORY_LOCK_KEYS
from . import serializers from . import serializers
@ -160,12 +161,15 @@ class AvailableIPsMixin:
# Determine the maximum number of IPs to return # Determine the maximum number of IPs to return
else: else:
config = Config()
PAGINATE_COUNT = config.PAGINATE_COUNT
MAX_PAGE_SIZE = config.MAX_PAGE_SIZE
try: try:
limit = int(request.query_params.get('limit', settings.PAGINATE_COUNT)) limit = int(request.query_params.get('limit', PAGINATE_COUNT))
except ValueError: except ValueError:
limit = settings.PAGINATE_COUNT limit = PAGINATE_COUNT
if settings.MAX_PAGE_SIZE: if MAX_PAGE_SIZE:
limit = min(limit, settings.MAX_PAGE_SIZE) limit = min(limit, MAX_PAGE_SIZE)
# Calculate available IPs within the parent # Calculate available IPs within the parent
ip_list = [] ip_list = []

View File

@ -2,6 +2,8 @@ from django.conf import settings
from django.db.models import QuerySet from django.db.models import QuerySet
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from netbox.config import Config
class OptionalLimitOffsetPagination(LimitOffsetPagination): class OptionalLimitOffsetPagination(LimitOffsetPagination):
""" """
@ -9,6 +11,8 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
matching a query, but retains the same format as a paginated request. The limit can only be disabled if matching a query, but retains the same format as a paginated request. The limit can only be disabled if
MAX_PAGE_SIZE has been set to 0 or None. MAX_PAGE_SIZE has been set to 0 or None.
""" """
def __init__(self):
self.default_limit = Config().PAGINATE_COUNT
def paginate_queryset(self, queryset, request, view=None): def paginate_queryset(self, queryset, request, view=None):
@ -40,11 +44,9 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
if limit < 0: if limit < 0:
raise ValueError() raise ValueError()
# Enforce maximum page size, if defined # Enforce maximum page size, if defined
if settings.MAX_PAGE_SIZE: MAX_PAGE_SIZE = Config().MAX_PAGE_SIZE
if limit == 0: if MAX_PAGE_SIZE:
return settings.MAX_PAGE_SIZE return MAX_PAGE_SIZE if limit == 0 else min(limit, MAX_PAGE_SIZE)
else:
return min(limit, settings.MAX_PAGE_SIZE)
return limit return limit
except (KeyError, ValueError): except (KeyError, ValueError):
pass pass

View File

@ -82,6 +82,20 @@ PARAMS = (
field_kwargs={'base_field': forms.CharField()} field_kwargs={'base_field': forms.CharField()}
), ),
# Pagination
ConfigParam(
name='PAGINATE_COUNT',
label='Default page size',
default=50,
field=forms.IntegerField
),
ConfigParam(
name='MAX_PAGE_SIZE',
label='Maximum page size',
default=1000,
field=forms.IntegerField
),
# Miscellaneous # Miscellaneous
ConfigParam( ConfigParam(
name='MAINTENANCE_MODE', name='MAINTENANCE_MODE',

View File

@ -158,11 +158,6 @@ LOGIN_REQUIRED = False
# re-authenticate. (Default: 1209600 [14 days]) # re-authenticate. (Default: 1209600 [14 days])
LOGIN_TIMEOUT = None LOGIN_TIMEOUT = None
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
# all objects by specifying "?limit=0".
MAX_PAGE_SIZE = 1000
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
# the default value of this setting is derived from the installed location. # the default value of this setting is derived from the installed location.
# MEDIA_ROOT = '/opt/netbox/netbox/media' # MEDIA_ROOT = '/opt/netbox/netbox/media'
@ -191,9 +186,6 @@ NAPALM_TIMEOUT = 30
# be provided as a dictionary. # be provided as a dictionary.
NAPALM_ARGS = {} NAPALM_ARGS = {}
# Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = 50
# Enable installed plugins. Add the name of each plugin to the list. # Enable installed plugins. Add the name of each plugin to the list.
PLUGINS = [] PLUGINS = []

View File

@ -75,6 +75,7 @@ ADMINS = getattr(configuration, 'ADMINS', [])
BASE_PATH = getattr(configuration, 'BASE_PATH', '') BASE_PATH = getattr(configuration, 'BASE_PATH', '')
if BASE_PATH: if BASE_PATH:
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
CHANGELOG_RETENTION = getattr(configuration, 'CHANGELOG_RETENTION', 90)
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False) CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', []) CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
@ -85,12 +86,19 @@ DEBUG = getattr(configuration, 'DEBUG', False)
DEVELOPER = getattr(configuration, 'DEVELOPER', False) DEVELOPER = getattr(configuration, 'DEVELOPER', False)
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs')) DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
EMAIL = getattr(configuration, 'EMAIL', {}) EMAIL = getattr(configuration, 'EMAIL', {})
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
GRAPHQL_ENABLED = getattr(configuration, 'GRAPHQL_ENABLED', True)
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None) HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1')) INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
LOGGING = getattr(configuration, 'LOGGING', {}) LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/') MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
PLUGINS = getattr(configuration, 'PLUGINS', []) PLUGINS = getattr(configuration, 'PLUGINS', [])
PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {}) PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False) REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend') REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', []) REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
@ -122,20 +130,10 @@ for param in PARAMS:
if hasattr(configuration, param.name): if hasattr(configuration, param.name):
globals()[param.name] = getattr(configuration, param.name) globals()[param.name] = getattr(configuration, param.name)
CHANGELOG_RETENTION = getattr(configuration, 'CHANGELOG_RETENTION', 90)
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
GRAPHQL_ENABLED = getattr(configuration, 'GRAPHQL_ENABLED', True)
LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {}) NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '') NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30) NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '') NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
# Validate update repo URL and timeout # Validate update repo URL and timeout
if RELEASE_CHECK_URL: if RELEASE_CHECK_URL:
@ -462,7 +460,7 @@ REST_FRAMEWORK = {
), ),
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION, 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'PAGE_SIZE': PAGINATE_COUNT, # 'PAGE_SIZE': PAGINATE_COUNT,
'SCHEMA_COERCE_METHOD_NAMES': { 'SCHEMA_COERCE_METHOD_NAMES': {
# Default mappings # Default mappings
'retrieve': 'read', 'retrieve': 'read',
@ -561,23 +559,6 @@ RQ_QUEUES = {
} }
#
# NetBox internal settings
#
# Pagination
if MAX_PAGE_SIZE and PAGINATE_COUNT > MAX_PAGE_SIZE:
raise ImproperlyConfigured(
f"PAGINATE_COUNT ({PAGINATE_COUNT}) must be less than or equal to MAX_PAGE_SIZE ({MAX_PAGE_SIZE}), if set."
)
PER_PAGE_DEFAULTS = [
25, 50, 100, 250, 500, 1000
]
if PAGINATE_COUNT not in PER_PAGE_DEFAULTS:
PER_PAGE_DEFAULTS.append(PAGINATE_COUNT)
PER_PAGE_DEFAULTS = sorted(PER_PAGE_DEFAULTS)
# #
# Plugins # Plugins
# #

View File

@ -36,7 +36,7 @@
{% endfor %} {% endfor %}
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<select name="per_page" class="form-select per-page"> <select name="per_page" class="form-select per-page">
{% for n in settings.PER_PAGE_DEFAULTS %} {% for n in page.paginator.get_page_lengths %}
<option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option> <option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,8 +1,12 @@
from django.conf import settings
from django.core.paginator import Paginator, Page from django.core.paginator import Paginator, Page
from netbox.config import Config
class EnhancedPaginator(Paginator): class EnhancedPaginator(Paginator):
default_page_lengths = (
25, 50, 100, 250, 500, 1000
)
def __init__(self, object_list, per_page, orphans=None, **kwargs): def __init__(self, object_list, per_page, orphans=None, **kwargs):
@ -10,9 +14,9 @@ class EnhancedPaginator(Paginator):
try: try:
per_page = int(per_page) per_page = int(per_page)
if per_page < 1: if per_page < 1:
per_page = settings.PAGINATE_COUNT per_page = Config().PAGINATE_COUNT
except ValueError: except ValueError:
per_page = settings.PAGINATE_COUNT per_page = Config().PAGINATE_COUNT
# Set orphans count based on page size # Set orphans count based on page size
if orphans is None and per_page <= 50: if orphans is None and per_page <= 50:
@ -25,6 +29,11 @@ class EnhancedPaginator(Paginator):
def _get_page(self, *args, **kwargs): def _get_page(self, *args, **kwargs):
return EnhancedPage(*args, **kwargs) return EnhancedPage(*args, **kwargs)
def get_page_lengths(self):
if self.per_page not in self.default_page_lengths:
return sorted([*self.default_page_lengths, self.per_page])
return self.default_page_lengths
class EnhancedPage(Page): class EnhancedPage(Page):
@ -57,17 +66,19 @@ def get_paginate_count(request):
Return the lesser of the calculated value and MAX_PAGE_SIZE. Return the lesser of the calculated value and MAX_PAGE_SIZE.
""" """
config = Config()
if 'per_page' in request.GET: if 'per_page' in request.GET:
try: try:
per_page = int(request.GET.get('per_page')) per_page = int(request.GET.get('per_page'))
if request.user.is_authenticated: if request.user.is_authenticated:
request.user.config.set('pagination.per_page', per_page, commit=True) request.user.config.set('pagination.per_page', per_page, commit=True)
return min(per_page, settings.MAX_PAGE_SIZE) return min(per_page, config.MAX_PAGE_SIZE)
except ValueError: except ValueError:
pass pass
if request.user.is_authenticated: if request.user.is_authenticated:
per_page = request.user.config.get('pagination.per_page', settings.PAGINATE_COUNT) per_page = request.user.config.get('pagination.per_page', config.PAGINATE_COUNT)
return min(per_page, settings.MAX_PAGE_SIZE) return min(per_page, config.MAX_PAGE_SIZE)
return min(settings.PAGINATE_COUNT, settings.MAX_PAGE_SIZE) return min(config.PAGINATE_COUNT, config.MAX_PAGE_SIZE)

View File

@ -1,6 +1,5 @@
import urllib.parse import urllib.parse
from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import Client, TestCase, override_settings from django.test import Client, TestCase, override_settings
from django.urls import reverse from django.urls import reverse
@ -10,6 +9,7 @@ from dcim.models import Region, Site
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField from extras.models import CustomField
from ipam.models import VLAN from ipam.models import VLAN
from netbox.config import Config
from utilities.testing import APITestCase, disable_warnings from utilities.testing import APITestCase, disable_warnings
@ -137,7 +137,7 @@ class APIPaginationTestCase(APITestCase):
def test_default_page_size(self): def test_default_page_size(self):
response = self.client.get(self.url, format='json', **self.header) response = self.client.get(self.url, format='json', **self.header)
page_size = settings.PAGINATE_COUNT page_size = Config().PAGINATE_COUNT
self.assertLess(page_size, 100, "Default page size not sufficient for data set") self.assertLess(page_size, 100, "Default page size not sufficient for data set")
self.assertHttpStatus(response, status.HTTP_200_OK) self.assertHttpStatus(response, status.HTTP_200_OK)