├── example ├── conf │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── app │ └── example │ │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ │ ├── models.py │ │ └── admin.py ├── requirements.txt ├── django-admin-inline-paginator-plus.png ├── manage.py ├── makefile └── fixtures │ └── bkp.json ├── django_admin_inline_paginator_plus ├── __init__.py ├── templatetags │ ├── __init__.py │ └── paginated_inline.py ├── apps.py ├── templates │ └── admin │ │ ├── stacked_paginated.html │ │ ├── tabular_paginated.html │ │ ├── paginated_base.html │ │ ├── stacked_paginator.html │ │ └── tabular_paginator.html ├── static │ └── django_admin_inline_paginator_plus │ │ └── paginator.css └── admin.py ├── MANIFEST.in ├── .editorconfig ├── .github └── .github │ └── PULL_REQUEST_TEMPLATE.md ├── pytest.ini ├── tests ├── __init__.py ├── apps_test.py └── admin_unit_test.py ├── .pre-commit-config.yaml ├── LICENSE ├── tox.ini ├── CHANGELOG.md ├── pyproject.toml ├── README.md └── .gitignore /example/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/app/example/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | django>=3.2 2 | django-extensions # just for `shell_plus` command 3 | -e .. # install 'django_admin_inline_paginator_plus' 4 | -------------------------------------------------------------------------------- /example/django-admin-inline-paginator-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DmytroLitvinov/django-admin-inline-paginator-plus/HEAD/example/django-admin-inline-paginator-plus.png -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.apps import AppConfig 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class DjangoAdminInlinePaginatorPlusConfig(AppConfig): 7 | name = 'django_admin_inline_paginator_plus' 8 | verbose_name = _('Django Admin Inline Paginator Plus') 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | 4 | recursive-include tests * 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | recursive-include django_admin_inline_paginator_plus/templates * 8 | recursive-include django_admin_inline_paginator_plus/static * 9 | recursive-include django_admin_inline_paginator_plus/locale * 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | [*.html] 14 | indent_size = 2 15 | 16 | [Makefile] 17 | indent_style = tab 18 | 19 | [*.bat] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.github/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also 4 | include relevant motivation and context. Your commit message should include 5 | this information as well. 6 | 7 | Fixes # (issue) 8 | 9 | # Checklist: 10 | 11 | - [ ] I have added the relevant tests for this change. 12 | - [ ] I have added an item to the Pending section of ``CHANGELOG.md``. 13 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/templates/admin/stacked_paginated.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/paginated_base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% include 'admin/edit_inline/stacked.html' %} 6 | 7 |
8 | {% include 'admin/stacked_paginator.html' %} 9 |
10 |
11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/templates/admin/tabular_paginated.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/paginated_base.html' %} 2 | 3 | {% block content %} 4 |
8 | {% include 'admin/edit_inline/tabular.html' %} 9 | 10 |
11 | {% include 'admin/tabular_paginator.html' %} 12 |
13 |
14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /example/conf/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/static/django_admin_inline_paginator_plus/paginator.css: -------------------------------------------------------------------------------- 1 | .btn-page { 2 | border: none; 3 | padding: 5px 10px; 4 | text-align: center; 5 | text-decoration: none; 6 | display: inline-block; 7 | font-size: 12px; 8 | margin: 4px 2px; 9 | cursor: pointer; 10 | } 11 | 12 | .page-selected { 13 | background-color: #ffffff; 14 | color: #666; 15 | } 16 | 17 | .page-available { 18 | background-color: #008cba; 19 | color: white !important; 20 | } 21 | 22 | .results { 23 | background-color: #ffffff; 24 | color: #666; 25 | } 26 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | xfail_strict=true 3 | testpaths=tests/ 4 | 5 | [pytest-watch] 6 | runner= pytest --failed-first --maxfail=1 7 | 8 | [MASTER] 9 | ignore = migrations,tests,manage.py 10 | load-plugins = 11 | pylint_common, 12 | pylint_django 13 | 14 | [MESSAGES CONTROL] 15 | disable= 16 | django-not-configured, 17 | missing-function-docstring, 18 | missing-class-docstring, 19 | missing-module-docstring, 20 | locally-disabled, 21 | too-few-public-methods 22 | 23 | [FORMAT] 24 | max-module-lines=1000 25 | max-line-length=120 26 | 27 | [DESIGN] 28 | max-args=10 29 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | 4 | import os 5 | import sys 6 | 7 | 8 | def main(): 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | 'available on your PYTHONPATH environment variable? Did you ' 16 | 'forget to activate a virtual environment?' 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/templatetags/paginated_inline.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | 3 | from django import template 4 | from django.utils.safestring import SafeString 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.simple_tag 10 | def modify_pagination_path(full_path: str, key: str, value: str) -> str: 11 | get_params = full_path 12 | if get_params.find('?') > -1: 13 | get_params = get_params[get_params.find('?') + 1 :] 14 | if get_params.find('#') > -1: 15 | get_params = get_params[: get_params.find('#')] 16 | 17 | params = urllib.parse.parse_qs(get_params) 18 | params[key] = [str(value)] 19 | return urllib.parse.urlencode(params, doseq=True) 20 | 21 | 22 | @register.simple_tag 23 | def hx_vals(key: str, value): 24 | return SafeString(f'hx-vals=\'{{"{key}": {value}}}\'') 25 | -------------------------------------------------------------------------------- /example/conf/urls.py: -------------------------------------------------------------------------------- 1 | """example URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.contrib import admin 18 | from django.urls import path 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | from django.conf import settings 5 | 6 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | 9 | settings.configure() 10 | 11 | settings.INSTALLED_APPS = [ 12 | # Django apps 13 | 'django.contrib.admin', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.staticfiles', 16 | # Third part apps 17 | 'django_admin_inline_paginator_plus', 18 | 'tests', 19 | ] 20 | 21 | settings.TEMPLATES = [ 22 | { 23 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 24 | 'DIRS': [ 25 | os.path.join(BASE_DIR, 'templates'), 26 | ], 27 | 'APP_DIRS': True, 28 | }, 29 | ] 30 | 31 | settings.STATIC_URL = '/' 32 | django.setup() 33 | -------------------------------------------------------------------------------- /tests/apps_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.apps import AppConfig 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from django_admin_inline_paginator_plus.apps import DjangoAdminInlinePaginatorPlusConfig 7 | 8 | 9 | class TestDjangoAppConfig(unittest.TestCase): 10 | def test_valid_subclass_appconfig(self): 11 | self.assertEqual(issubclass(DjangoAdminInlinePaginatorPlusConfig, AppConfig), True) 12 | 13 | def test_valid_name(self): 14 | name = DjangoAdminInlinePaginatorPlusConfig.name 15 | self.assertEqual(isinstance(name, str), True) 16 | self.assertEqual(name, 'django_admin_inline_paginator_plus') 17 | 18 | def test_valid_verbose_name(self): 19 | verbose_name = DjangoAdminInlinePaginatorPlusConfig.verbose_name 20 | self.assertEqual(verbose_name, _('Django Admin Inline Paginator Plus')) 21 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/templates/admin/paginated_base.html: -------------------------------------------------------------------------------- 1 | {% if inline_admin_formset.formset.htmx_enabled %} 2 | 3 | {% endif %} 4 | 5 | 18 | 19 | {% block content %} 20 | {% endblock content %} 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.6.0 4 | hooks: 5 | - id: check-toml 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: mixed-line-ending 10 | - repo: https://github.com/adamchainz/django-upgrade 11 | rev: 1.19.0 12 | hooks: 13 | - id: django-upgrade 14 | args: [--target-version, "4.2"] 15 | - repo: https://github.com/astral-sh/ruff-pre-commit 16 | rev: 'v0.5.5' 17 | hooks: 18 | - id: ruff 19 | args: [--fix, --exit-non-zero-on-fix] 20 | - id: ruff-format 21 | #- repo: https://github.com/tox-dev/pyproject-fmt 22 | # rev: 2.1.4 23 | # hooks: 24 | # - id: pyproject-fmt 25 | #- repo: https://github.com/abravalheri/validate-pyproject 26 | # rev: v0.18 27 | # hooks: 28 | # - id: validate-pyproject 29 | -------------------------------------------------------------------------------- /example/makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --silent # No print command executed 2 | ARGS = $(filter-out $@,$(MAKECMDGOALS)) 3 | 4 | ##### 5 | ## Utils 6 | ### 7 | 8 | check_virtualenv: 9 | # if is not a docker, activate virtualenv 10 | if [ ! -f /.dockerenv ] && [ -z "${VIRTUAL_ENV}" ] ; then \ 11 | echo "\033[0;31mError: No Virtualenv activated.${NC}"; \ 12 | exit 1; \ 13 | fi 14 | 15 | ##### 16 | ## Run 17 | ### 18 | 19 | before_run: check_virtualenv 20 | pip install -r requirements.txt 21 | python manage.py migrate 22 | 23 | run: before_run 24 | python manage.py runserver 0:8000 25 | 26 | ##### 27 | ## Shortcuts 28 | ### 29 | 30 | dj: check_virtualenv 31 | python manage.py "${ARGS}" 32 | 33 | dump_data: check_virtualenv 34 | python manage.py dumpdata --indent 4 --exclude admin.logentry --exclude auth.permission --exclude auth.group --exclude contenttypes.contenttype --exclude sessions.session> fixtures/bkp.json 35 | 36 | load_data: check_virtualenv 37 | python manage.py loaddata fixtures/bkp.json 38 | -------------------------------------------------------------------------------- /example/app/example/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db.models import CASCADE, BooleanField, CharField, ForeignKey, Model 3 | 4 | 5 | class Country(Model): 6 | name = CharField(max_length=100) 7 | active = BooleanField(default=True) 8 | 9 | def __str__(self): 10 | return self.name 11 | 12 | class Meta: 13 | verbose_name = 'Country' 14 | verbose_name_plural = 'Countries' 15 | 16 | 17 | class State(Model): 18 | country = ForeignKey('example.Country', on_delete=CASCADE) 19 | name = CharField(max_length=100) 20 | active = BooleanField(default=True) 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | class Meta: 26 | verbose_name = 'State' 27 | verbose_name_plural = 'States' 28 | 29 | 30 | class Region(Model): 31 | country = ForeignKey('example.Country', on_delete=CASCADE) 32 | name = CharField(max_length=100) 33 | active = BooleanField(default=True) 34 | 35 | def __str__(self): 36 | return self.name 37 | 38 | class Meta: 39 | verbose_name = 'Region' 40 | verbose_name_plural = 'Regions' 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dmytro Litvinov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py39-django{42} 3 | py310-django{42,50,51,52} 4 | py311-django{42,50,51,52} 5 | py312-django{42,50,51,52} 6 | 7 | [gh-actions] 8 | python = 9 | 3.9: py39 10 | 3.10: py310 11 | 3.11: py311 12 | 3.12: py312 13 | 3.13: py313 14 | 15 | [testenv] 16 | commands = 17 | python \ 18 | -W error::ResourceWarning \ 19 | -W error::DeprecationWarning \ 20 | -W error::PendingDeprecationWarning \ 21 | -m pytest \ 22 | --cov=django_admin_inline_paginator_plus \ 23 | --cov-config=tox.ini \ 24 | --cov-fail-under=35 \ 25 | --cov-report=term-missing \ 26 | --cov-report=xml:coverage.xml \ 27 | --durations=10 \ 28 | --cov-append 29 | extras = dev 30 | deps = 31 | pytest 32 | pytest-cov 33 | django42: Django>=4.2,<5.0 34 | django50: Django>=5.0a1,<5.1 35 | django51: Django>=5.1a1,<5.2 36 | django52: Django>=5.2a1,<6.0 37 | 38 | [coverage:run] 39 | relative_files = True 40 | source = django_admin_inline_paginator_plus/ 41 | branch = True 42 | 43 | [testenv:report] 44 | deps = coverage 45 | skip_install = true 46 | commands = 47 | coverage report 48 | coverage html 49 | -------------------------------------------------------------------------------- /example/app/example/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import ModelAdmin, register 2 | from django_admin_inline_paginator_plus.admin import ( 3 | StackedInlinePaginated, 4 | TabularInlinePaginated, 5 | ) 6 | 7 | from .models import Country, Region, State 8 | 9 | 10 | class StateAdminInline(TabularInlinePaginated): 11 | model = State 12 | fields = ('name', 'active') 13 | per_page = 5 14 | pagination_key = 'state_page' 15 | 16 | 17 | class CollapsedStateAdminInline(StateAdminInline): 18 | verbose_name = 'State Collapsed' 19 | verbose_name_plural = 'States Collapsed' 20 | pagination_key = 'state_collapsed_page' 21 | classes = ['collapse'] 22 | 23 | 24 | class RegionAdminInline(StackedInlinePaginated): 25 | model = Region 26 | fields = ('name', 'active') 27 | per_page = 2 28 | pagination_key = 'region_page' 29 | 30 | 31 | @register(Country) 32 | class CountryAdmin(ModelAdmin): 33 | model = Country 34 | list_display = ('name', 'active') 35 | fields = ('name', 'active') 36 | inlines = (StateAdminInline, CollapsedStateAdminInline, RegionAdminInline) 37 | list_filter = ('active',) 38 | 39 | 40 | @register(State) 41 | class StateAdmin(ModelAdmin): 42 | pass 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | 5 | ## [0.1.4] - 27-03-2025 6 | 7 | * ### Changed 8 | - Nothing new - just improve README and CHANGELOG's link and tested versions for PyPI package 9 | 10 | ## [0.1.3] - 22-02-2025 11 | 12 | * ### Fixed 13 | - Fix URL params in htmx, restore no JS support. More details in [pr #4](https://github.com/DmytroLitvinov/django-admin-inline-paginator-plus/pull/4) 14 | - Thanks to @pmdevita for these recent releases 👍 15 | 16 | ## [0.1.2] - 22-02-2025 17 | 18 | * ### Fixed 19 | - Fix deleting an item from a subsequent page of TabularInlinePaginated when GET params are presented. More details in [issue #3](https://github.com/DmytroLitvinov/django-admin-inline-paginator-plus/issues/3) 20 | 21 | ## [0.1.1] - 26-07-2024 22 | 23 | * ### Added 24 | - Add classifiers for Django versions 25 | * ### Changed 26 | - Change Development status from Beta to Production/Stable 27 | 28 | ## [0.1.0] - 10-07-2024 29 | 30 | New forked version `django-admin-inline-paginator-plus` 🎉 31 | 32 | * ### Added 33 | - Add StackedInlinePaginated 34 | - Add `htmx` for AJAX-paginated support 35 | * ### Changed 36 | * ### Fixed 37 | * ### Credits 38 | - Thanks to @jazzband for the original `django-admin-inline-paginator` project 39 | -------------------------------------------------------------------------------- /tests/admin_unit_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.contrib.admin.views.main import ChangeList 4 | 5 | from django_admin_inline_paginator_plus.admin import ( 6 | InlineChangeList, 7 | PaginationFormSetBase, 8 | ) 9 | 10 | 11 | class TestInlineChangeList(unittest.TestCase): 12 | """Test cases for InlineChangeList admin""" 13 | 14 | def test_default_values(self): 15 | self.assertEqual(InlineChangeList.can_show_all, True) 16 | self.assertEqual(InlineChangeList.multi_page, True) 17 | self.assertEqual(InlineChangeList.get_query_string, ChangeList.__dict__['get_query_string']) 18 | 19 | def test_init_values(self): 20 | """Test case for correct initialization class""" 21 | pass 22 | 23 | # cl = InlineChangeList(request, page_num, paginator) 24 | # cl.page_num = page_num 25 | # cl.paginator = paginator 26 | # cl.result_count = paginator.count 27 | 28 | # cl.show_all = 'all' in request.GET 29 | # cl.params = dict(request.GET.items()) 30 | 31 | 32 | class TestPaginationFormSetBase(unittest.TestCase): 33 | def test_default_values(self): 34 | """Test case to check if the default was a value set""" 35 | self.assertEqual(PaginationFormSetBase.queryset, None) 36 | self.assertEqual(PaginationFormSetBase.request, None) 37 | self.assertEqual(PaginationFormSetBase.per_page, 20) 38 | 39 | def test_get_page_num(self): 40 | """Test case to check correct getting page number""" 41 | pass 42 | 43 | def test_get_page(self): 44 | """Test case to check correct getting page""" 45 | pass 46 | 47 | def test_mount_paginator(self): 48 | """Test case for method mount_paginator""" 49 | pass 50 | 51 | def test_mount_queryset(self): 52 | """Test case for method mount_queryset""" 53 | pass 54 | 55 | def test_init(self): 56 | """Test case for correct initialization class""" 57 | pass 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "django-admin-inline-paginator-plus" 7 | version = "0.1.4" 8 | requires-python = ">=3.8" 9 | description = "The 'Django Admin Inline Paginator Plus' is simple way to paginate your inlines in Django admin" 10 | readme = "README.md" 11 | keywords = ["django", "admin", "paginator", "inlines", "pagination"] 12 | authors = [ 13 | {name = "Shinneider Libanio da Silva", email = "shinneider-libanio@hotmail.com"}, 14 | {name = "Dmytro Litvinov", email = "me@dmytrolitvinov.com"} 15 | ] 16 | maintainers = [ 17 | {name = "Dmytro Litvinov", email = "me@dmytrolitvinov.com"} 18 | ] 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Framework :: Django :: 4.2", 22 | "Framework :: Django :: 5.0", 23 | "Framework :: Django :: 5.1", 24 | "Framework :: Django :: 5.2", 25 | "Environment :: Web Environment", 26 | "Framework :: Django", 27 | "Intended Audience :: Developers", 28 | "License :: OSI Approved :: MIT License", 29 | "Natural Language :: English", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3 :: Only", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Programming Language :: Python :: 3.13", 38 | "Topic :: Internet :: WWW/HTTP", 39 | "Topic :: Software Development :: Libraries :: Python Modules" 40 | ] 41 | license = {text = "MIT"} 42 | dependencies = [ 43 | "django" 44 | ] 45 | 46 | [project.optional-dependencies] 47 | dev = [ 48 | "coverage", # testing 49 | "mypy", # linting 50 | "pytest", # testing 51 | "ruff" # linting 52 | ] 53 | 54 | [project.urls] 55 | Homepage = "https://github.com/DmytroLitvinov/django-admin-inline-paginator-plus" 56 | Issues = "https://github.com/DmytroLitvinov/django-admin-inline-paginator-plus/issues" 57 | Changelog = "https://github.com/DmytroLitvinov/django-admin-inline-paginator-plus/blob/master/CHANGELOG.md" 58 | 59 | [tool.setuptools.packages.find] 60 | include = ["django_admin_inline_paginator_plus*"] 61 | 62 | # Ruff 63 | # ---- 64 | [tool.ruff] 65 | src = ["django_admin_inline_paginator_plus"] 66 | line-length = 120 67 | indent-width = 4 68 | 69 | [tool.ruff.format] 70 | quote-style = "single" 71 | indent-style = "space" 72 | skip-magic-trailing-comma = false 73 | line-ending = "auto" 74 | 75 | # Mypy 76 | # ---- 77 | 78 | [tool.mypy] 79 | files = "." 80 | 81 | # Use strict defaults 82 | strict = true 83 | warn_unreachable = true 84 | warn_no_return = true 85 | 86 | [[tool.mypy.overrides]] 87 | # Don't require test functions to include types 88 | module = "tests.*" 89 | allow_untyped_defs = true 90 | disable_error_code = "attr-defined" 91 | -------------------------------------------------------------------------------- /example/app/example/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-06-08 02:20 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Country', 15 | fields=[ 16 | ( 17 | 'id', 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name='ID', 23 | ), 24 | ), 25 | ('name', models.CharField(max_length=100)), 26 | ('active', models.BooleanField(default=True)), 27 | ], 28 | options={ 29 | 'verbose_name': 'Country', 30 | 'verbose_name_plural': 'Countries', 31 | }, 32 | ), 33 | migrations.CreateModel( 34 | name='State', 35 | fields=[ 36 | ( 37 | 'id', 38 | models.AutoField( 39 | auto_created=True, 40 | primary_key=True, 41 | serialize=False, 42 | verbose_name='ID', 43 | ), 44 | ), 45 | ('name', models.CharField(max_length=100)), 46 | ('active', models.BooleanField(default=True)), 47 | ( 48 | 'country', 49 | models.ForeignKey( 50 | on_delete=django.db.models.deletion.CASCADE, 51 | to='example.Country', 52 | ), 53 | ), 54 | ], 55 | options={ 56 | 'verbose_name': 'State', 57 | 'verbose_name_plural': 'States', 58 | }, 59 | ), 60 | migrations.CreateModel( 61 | name='Region', 62 | fields=[ 63 | ( 64 | 'id', 65 | models.AutoField( 66 | auto_created=True, 67 | primary_key=True, 68 | serialize=False, 69 | verbose_name='ID', 70 | ), 71 | ), 72 | ('name', models.CharField(max_length=100)), 73 | ('active', models.BooleanField(default=True)), 74 | ( 75 | 'country', 76 | models.ForeignKey( 77 | on_delete=django.db.models.deletion.CASCADE, 78 | to='example.Country', 79 | ), 80 | ), 81 | ], 82 | options={ 83 | 'verbose_name': 'Region', 84 | 'verbose_name_plural': 'Regions', 85 | }, 86 | ), 87 | ] 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Admin Inline Paginator Plus ⏩ 2 | ===================================== 3 | 4 | **🍴 This is a forked and updated version based on original library [django-admin-inline-paginator](https://github.com/shinneider/django-admin-inline-paginator).** 5 | 6 | > *As for 10.07.2024 nobody took responsibility, so I decided to take it since we need additional functionlity like AJAX for pagination.* 7 | 8 | The **"Django Admin Inline Paginator Plus"** is simple way to paginate your inline in django admin 9 | 10 | To keep Django ecosystem fresh and updated, please share your love and support, click `Star` 🫶 11 | 12 | ## Features 13 | - **Easy Inline Pagination:** Quickly paginate inlines in the Django admin. 14 | - **AJAX Support:** Smooth and dynamic pagination without page reloads with `htmx`. 15 | - **Multiple Inline Pagination:** Manage multiple paginated inlines seamlessly. 16 | 17 | 18 | Here's a screenshot of the paginated inlines in action: 19 | 20 | ![Django Admin Inline Paginator Plus screenshot](https://github.com/DmytroLitvinov/django-admin-inline-paginator-plus/blob/bd4d0eb435ae86b37473044a6d192405c3f0c86f/example/django-admin-inline-paginator-plus.png "Screenshot of Django Admin Inline Paginator Plus") 21 | 22 | 23 | # Install: 24 | 25 | Install the package via pip: 26 | 27 | ```bash 28 | pip install django-admin-inline-paginator-plus 29 | ``` 30 | 31 | # Usage: 32 | 33 | 1. Add to your INSTALLED_APPS, in settings.py: 34 | 35 | ```python 36 | INSTALLED_APPS = [ 37 | ... 38 | 'django_admin_inline_paginator_plus', 39 | ... 40 | ] 41 | ``` 42 | 2. Create your model inline: 43 | 44 | You can use `TabularInlinePaginated` ot `StackedInlinePaginated`. In our example we use `TabularInlinePaginated`. 45 | 46 | ```python 47 | from django_admin_inline_paginator_plus.admin import TabularInlinePaginated 48 | 49 | class ModelWithFKAdminInline(TabularInlinePaginated): 50 | model = ModelWithFK 51 | fields = (...) 52 | per_page = 5 53 | ``` 54 | 55 | 3. Create main model admin and use inline: 56 | 57 | ```python 58 | @register(YourModel) 59 | class YourModelAdmin(ModelAdmin): 60 | model = YourModel 61 | fields = (...) 62 | inlines = (ModelWithFKAdminInline, ) 63 | ``` 64 | 65 | # Advanced Usage: 66 | 67 | 1. Paginate multiples inlines: 68 | 69 | ```python 70 | from django_admin_inline_paginator_plus.admin import TabularInlinePaginated, StackedInlinePaginated 71 | 72 | class ModelWithFKInline(TabularInlinePaginated): 73 | model = ModelWithFK 74 | fields = ('name', 'active') 75 | per_page = 2 76 | pagination_key = 'page-model' # make sure it's unique for page inline 77 | 78 | class AnotherModelWithFKInline(StackedInlinePaginated): 79 | model = AnotherModelWithFK 80 | fields = ('name', 'active') 81 | per_page = 2 82 | pagination_key = 'page-another-model' # make sure it's unique for page inline 83 | ``` 84 | 85 | 2. Use inlines from step 1. and add to your main model admin: 86 | 87 | ```python 88 | @register(YourModel) 89 | class YourModelAdmin(ModelAdmin): 90 | model = YourModel 91 | fields = (...) 92 | inlines = (ModelWithFKAdminInline, AnotherModelWithFKInline) 93 | ``` 94 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/admin.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from django.contrib.admin import StackedInline, TabularInline 4 | from django.contrib.admin.views.main import ChangeList 5 | from django.contrib.contenttypes.admin import GenericTabularInline 6 | from django.core.paginator import Paginator 7 | from django.db.models import QuerySet 8 | from django.http import HttpRequest 9 | 10 | 11 | class InlineChangeList: 12 | """ 13 | Used by template to construct the paginator 14 | """ 15 | 16 | can_show_all = True 17 | multi_page = True 18 | get_query_string = ChangeList.__dict__['get_query_string'] 19 | 20 | def __init__(self, request: HttpRequest, page_num: int, paginator: Paginator): 21 | self.show_all = 'all' in request.GET 22 | self.page_num = page_num 23 | self.paginator = paginator 24 | self.result_count = paginator.count 25 | self.params = dict(request.GET.items()) 26 | 27 | 28 | class PaginationFormSetBase: 29 | queryset: Optional[QuerySet] = None 30 | request: Optional[HttpRequest] = None 31 | per_page = 20 32 | pagination_key = 'page' 33 | htmx_enabled = True 34 | 35 | def __init__(self, *args, **kwargs): 36 | super().__init__(*args, **kwargs) 37 | self.mount_paginator() 38 | self.mount_queryset() 39 | 40 | def get_page_num(self) -> int: 41 | assert self.request is not None 42 | page = self.request.GET.get(self.pagination_key) 43 | if page and page.isnumeric() and page > '0': 44 | return int(page) 45 | page = self.request.POST.get(f"_paginator-plus-{self.prefix}") 46 | if page and page.isnumeric() and page > '0': 47 | return int(page) 48 | 49 | return 1 50 | 51 | def get_page(self, paginator: Paginator, page: int): 52 | if page <= paginator.num_pages: 53 | return paginator.page(page) 54 | 55 | return paginator.page(1) 56 | 57 | def mount_paginator(self, page_num: int = None): 58 | assert self.queryset is not None and self.request is not None 59 | 60 | page_num = self.get_page_num() if not page_num else page_num 61 | self.paginator = Paginator(self.queryset, self.per_page) 62 | self.page = self.get_page(self.paginator, page_num) 63 | self.cl = InlineChangeList(self.request, page_num, self.paginator) 64 | 65 | def mount_queryset(self): 66 | if self.cl.show_all: 67 | self._queryset = self.queryset 68 | 69 | self._queryset = self.page.object_list 70 | 71 | 72 | class InlinePaginated: 73 | pagination_key = 'page' 74 | template = 'admin/tabular_paginated.html' 75 | per_page = 20 76 | extra = 0 77 | htmx_enabled = True 78 | 79 | def get_formset(self, request, obj=None, **kwargs): 80 | formset_class = super().get_formset(request, obj, **kwargs) 81 | 82 | class PaginationFormSet(PaginationFormSetBase, formset_class): 83 | pagination_key = self.pagination_key 84 | 85 | PaginationFormSet.request = request 86 | PaginationFormSet.per_page = self.per_page 87 | PaginationFormSet.htmx_enabled = self.htmx_enabled 88 | return PaginationFormSet 89 | 90 | 91 | class StackedInlinePaginated(InlinePaginated, StackedInline): 92 | template = 'admin/stacked_paginated.html' 93 | 94 | 95 | class TabularInlinePaginated(InlinePaginated, TabularInline): 96 | pass 97 | 98 | 99 | class GenericTabularInlinePaginated(InlinePaginated, GenericTabularInline): 100 | pass 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | cover/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | .pybuilder/ 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | # For a library or package, you might want to ignore these files since the code is 86 | # intended to run in multiple environments; otherwise, check them in: 87 | # .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # poetry 97 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 98 | # This is especially recommended for binary packages to ensure reproducibility, and is more 99 | # commonly ignored for libraries. 100 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 101 | #poetry.lock 102 | 103 | # pdm 104 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 105 | #pdm.lock 106 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 107 | # in version control. 108 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 109 | .pdm.toml 110 | .pdm-python 111 | .pdm-build/ 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | .idea/ 162 | 163 | # Others 164 | .DS_STORE 165 | -------------------------------------------------------------------------------- /example/conf/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from typing import List 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'wa@as#_q*^g$i1u4bvl*_=8v=s6=(_4)$&g3d73g&z%$$gj94*' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS: List[str] = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | # Django apps 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | # Third part apps 43 | 'django_admin_inline_paginator_plus', # our tested library 44 | 'django_extensions', # for `python manage.py shell_plus` command 45 | # Developed apps 46 | 'app.example', 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'conf.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [ 65 | os.path.join(BASE_DIR, 'templates'), 66 | ], 67 | 'APP_DIRS': True, 68 | 'OPTIONS': { 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | ], 75 | }, 76 | }, 77 | ] 78 | 79 | WSGI_APPLICATION = 'conf.wsgi.application' 80 | 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 84 | 85 | DATABASES = { 86 | 'default': { 87 | 'ENGINE': 'django.db.backends.sqlite3', 88 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 89 | } 90 | } 91 | 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 108 | }, 109 | ] 110 | 111 | 112 | # Internationalization 113 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 114 | 115 | LANGUAGE_CODE = 'en-us' 116 | 117 | TIME_ZONE = 'UTC' 118 | 119 | USE_I18N = True 120 | 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/templates/admin/stacked_paginator.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load static %} 3 | {% load paginated_inline %} 4 | 5 | 6 | 7 |
13 | {% with inline_admin_formset.formset.page as page_obj %} 14 | 15 |

16 | {% if page_obj.has_previous %} 17 | {% translate 'previous' %} 23 | {% endif %} 24 | 25 | {% if page_obj.number|add:"-5" > 0 %} 26 | 1 32 | {% endif %} 33 | 34 | {% if page_obj.number|add:"-5" > 1 %} 35 | 36 | {% endif %} 37 | 38 | {% for page_num in page_obj.paginator.page_range %} 39 | {% if page_obj.number == page_num %} 40 | {{ page_num }} 41 | {% else %} 42 | {% if page_num > page_obj.number|add:"-5" and page_num < page_obj.number|add:"5" %} 43 | {{ page_num }} 49 | {% endif %} 50 | {% endif %} 51 | {% endfor %} 52 | 53 | {% if page_obj.number|add:"5" < page_obj.paginator.num_pages %} 54 | 55 | {% endif %} 56 | 57 | {% if page_obj.number|add:"4" < page_obj.paginator.num_pages %} 58 | {{ page_obj.paginator.num_pages }} 64 | {% endif %} 65 | 66 | {% if page_obj.has_next %} 67 | {% translate 'next' %} 73 | {% endif %} 74 | {{ page_obj.paginator.count }} {% translate 'Results' %} 75 |

76 | {% endwith %} 77 |
78 | -------------------------------------------------------------------------------- /django_admin_inline_paginator_plus/templates/admin/tabular_paginator.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load static %} 3 | {% load paginated_inline %} 4 | 5 | 6 | 7 | 8 |
14 | {% with inline_admin_formset.formset.page as page_obj %} 15 | 16 |

17 | {% if page_obj.has_previous %} 18 | 25 | {% translate 'previous' %} 26 | 27 | {% endif %} 28 | 29 | {% if page_obj.number|add:"-5" > 0 %} 30 | 36 | 1 37 | 38 | {% endif %} 39 | 40 | {% if page_obj.number|add:"-5" > 1 %} 41 | 42 | {% endif %} 43 | 44 | {% for page_num in page_obj.paginator.page_range %} 45 | {% if page_obj.number == page_num %} 46 | {{ page_num }} 47 | {% else %} 48 | {% if page_num > page_obj.number|add:"-5" and page_num < page_obj.number|add:"5" %} 49 | 55 | {{ page_num }} 56 | 57 | {% endif %} 58 | {% endif %} 59 | {% endfor %} 60 | 61 | {% if page_obj.number|add:"5" < page_obj.paginator.num_pages %} 62 | 63 | {% endif %} 64 | 65 | {% if page_obj.number|add:"4" < page_obj.paginator.num_pages %} 66 | 72 | {{ page_obj.paginator.num_pages }} 73 | 74 | {% endif %} 75 | 76 | {% if page_obj.has_next %} 77 | 84 | {% translate 'next' %} 85 | 86 | {% endif %} 87 | {{ page_obj.paginator.count }} {% translate 'Results' %} 88 |

89 | {% endwith %} 90 |
91 | -------------------------------------------------------------------------------- /example/fixtures/bkp.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "example.country", 4 | "pk": 1, 5 | "fields": { 6 | "name": "Brasil", 7 | "active": true 8 | } 9 | }, 10 | { 11 | "model": "example.state", 12 | "pk": 1, 13 | "fields": { 14 | "country": 1, 15 | "name": "Acre", 16 | "active": true 17 | } 18 | }, 19 | { 20 | "model": "example.state", 21 | "pk": 2, 22 | "fields": { 23 | "country": 1, 24 | "name": "Alagoas", 25 | "active": true 26 | } 27 | }, 28 | { 29 | "model": "example.state", 30 | "pk": 3, 31 | "fields": { 32 | "country": 1, 33 | "name": "Amap\u00e1", 34 | "active": true 35 | } 36 | }, 37 | { 38 | "model": "example.state", 39 | "pk": 4, 40 | "fields": { 41 | "country": 1, 42 | "name": "Amazonas", 43 | "active": true 44 | } 45 | }, 46 | { 47 | "model": "example.state", 48 | "pk": 5, 49 | "fields": { 50 | "country": 1, 51 | "name": "Bahia", 52 | "active": true 53 | } 54 | }, 55 | { 56 | "model": "example.state", 57 | "pk": 6, 58 | "fields": { 59 | "country": 1, 60 | "name": "Cear\u00e1", 61 | "active": true 62 | } 63 | }, 64 | { 65 | "model": "example.state", 66 | "pk": 7, 67 | "fields": { 68 | "country": 1, 69 | "name": "Distrito Federal", 70 | "active": true 71 | } 72 | }, 73 | { 74 | "model": "example.state", 75 | "pk": 8, 76 | "fields": { 77 | "country": 1, 78 | "name": "Esp\u00edrito Santo", 79 | "active": true 80 | } 81 | }, 82 | { 83 | "model": "example.state", 84 | "pk": 9, 85 | "fields": { 86 | "country": 1, 87 | "name": "Goi\u00e1s", 88 | "active": true 89 | } 90 | }, 91 | { 92 | "model": "example.state", 93 | "pk": 10, 94 | "fields": { 95 | "country": 1, 96 | "name": "Maranh\u00e3o", 97 | "active": true 98 | } 99 | }, 100 | { 101 | "model": "example.state", 102 | "pk": 11, 103 | "fields": { 104 | "country": 1, 105 | "name": "Mato Grosso", 106 | "active": true 107 | } 108 | }, 109 | { 110 | "model": "example.state", 111 | "pk": 12, 112 | "fields": { 113 | "country": 1, 114 | "name": "Mato Grosso do Sul", 115 | "active": true 116 | } 117 | }, 118 | { 119 | "model": "example.state", 120 | "pk": 13, 121 | "fields": { 122 | "country": 1, 123 | "name": "Minas Gerais", 124 | "active": true 125 | } 126 | }, 127 | { 128 | "model": "example.state", 129 | "pk": 14, 130 | "fields": { 131 | "country": 1, 132 | "name": "Par\u00e1", 133 | "active": true 134 | } 135 | }, 136 | { 137 | "model": "example.state", 138 | "pk": 15, 139 | "fields": { 140 | "country": 1, 141 | "name": "Para\u00edba", 142 | "active": true 143 | } 144 | }, 145 | { 146 | "model": "example.state", 147 | "pk": 16, 148 | "fields": { 149 | "country": 1, 150 | "name": "Paran\u00e1", 151 | "active": true 152 | } 153 | }, 154 | { 155 | "model": "example.state", 156 | "pk": 17, 157 | "fields": { 158 | "country": 1, 159 | "name": "Pernambuco", 160 | "active": true 161 | } 162 | }, 163 | { 164 | "model": "example.state", 165 | "pk": 18, 166 | "fields": { 167 | "country": 1, 168 | "name": "Piau\u00ed", 169 | "active": true 170 | } 171 | }, 172 | { 173 | "model": "example.state", 174 | "pk": 19, 175 | "fields": { 176 | "country": 1, 177 | "name": "Rio de Janeiro", 178 | "active": true 179 | } 180 | }, 181 | { 182 | "model": "example.state", 183 | "pk": 20, 184 | "fields": { 185 | "country": 1, 186 | "name": "Rio Grande do Norte", 187 | "active": true 188 | } 189 | }, 190 | { 191 | "model": "example.state", 192 | "pk": 21, 193 | "fields": { 194 | "country": 1, 195 | "name": "Rio Grande do Sul", 196 | "active": true 197 | } 198 | }, 199 | { 200 | "model": "example.state", 201 | "pk": 22, 202 | "fields": { 203 | "country": 1, 204 | "name": "Rond\u00f4nia", 205 | "active": true 206 | } 207 | }, 208 | { 209 | "model": "example.state", 210 | "pk": 23, 211 | "fields": { 212 | "country": 1, 213 | "name": "Roraima", 214 | "active": true 215 | } 216 | }, 217 | { 218 | "model": "example.state", 219 | "pk": 24, 220 | "fields": { 221 | "country": 1, 222 | "name": "Santa Catarina", 223 | "active": true 224 | } 225 | }, 226 | { 227 | "model": "example.state", 228 | "pk": 25, 229 | "fields": { 230 | "country": 1, 231 | "name": "S\u00e3o Paulo", 232 | "active": true 233 | } 234 | }, 235 | { 236 | "model": "example.state", 237 | "pk": 26, 238 | "fields": { 239 | "country": 1, 240 | "name": "Sergipe", 241 | "active": true 242 | } 243 | }, 244 | { 245 | "model": "example.state", 246 | "pk": 27, 247 | "fields": { 248 | "country": 1, 249 | "name": "Tocantins", 250 | "active": true 251 | } 252 | }, 253 | { 254 | "model": "example.region", 255 | "pk": 1, 256 | "fields": { 257 | "country": 1, 258 | "name": "Norte", 259 | "active": true 260 | } 261 | }, 262 | { 263 | "model": "example.region", 264 | "pk": 2, 265 | "fields": { 266 | "country": 1, 267 | "name": "Nordeste", 268 | "active": true 269 | } 270 | }, 271 | { 272 | "model": "example.region", 273 | "pk": 3, 274 | "fields": { 275 | "country": 1, 276 | "name": "Centro-Oeste", 277 | "active": true 278 | } 279 | }, 280 | { 281 | "model": "example.region", 282 | "pk": 4, 283 | "fields": { 284 | "country": 1, 285 | "name": "Sudeste", 286 | "active": true 287 | } 288 | }, 289 | { 290 | "model": "example.region", 291 | "pk": 5, 292 | "fields": { 293 | "country": 1, 294 | "name": "Sul", 295 | "active": true 296 | } 297 | }, 298 | { 299 | "model": "auth.user", 300 | "pk": 1, 301 | "fields": { 302 | "password": "pbkdf2_sha256$150000$JmlShwDi4gAa$7ctE0wVewpljxqhaxK95Jd8wxfTyUZskjd/8eND2OWQ=", 303 | "last_login": "2021-06-08T01:44:47.640Z", 304 | "is_superuser": true, 305 | "username": "admin", 306 | "first_name": "", 307 | "last_name": "", 308 | "email": "admin@admin.com", 309 | "is_staff": true, 310 | "is_active": true, 311 | "date_joined": "2021-06-08T01:44:37.956Z", 312 | "groups": [], 313 | "user_permissions": [] 314 | } 315 | } 316 | ] 317 | --------------------------------------------------------------------------------