├── .github └── workflows │ ├── code-quality.yml │ ├── on-push.yml │ ├── on-tag.yml │ ├── pypi.yml │ ├── sonar.yml │ └── tests.yml ├── .gitignore ├── .pylintrc ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── django_admin_inline_paginator ├── __init__.py ├── admin.py ├── apps.py ├── static │ └── django_admin_inline_paginator │ │ └── paginator.css ├── templates │ └── admin │ │ ├── tabular_paginated.html │ │ └── tabular_paginator.html └── templatetags │ ├── __init__.py │ └── paginated_inline.py ├── example ├── app │ └── example │ │ ├── admin.py │ │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ │ └── models.py ├── conf │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── fixtures │ └── bkp.json ├── makefile ├── manage.py └── requirements.txt ├── profile-bandit.yml ├── pytest.ini ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── admin_unit_test.py └── apps_test.py └── tox.ini /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Run Code Quality Tools 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | code-quality: 8 | name: Code Quality 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 3.10 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: "3.x" 17 | 18 | - name: Install dependencies 19 | run: | 20 | pip install ."[dev]" ."[code-quality]" 21 | 22 | - name: Run isort 23 | run: isort ./django_admin_inline_paginator --check-only --diff 24 | 25 | - name: Run xenon (Cyclomatic Complexity) 26 | run: xenon --max-absolute B --max-modules A --max-average A ./django_admin_inline_paginator 27 | 28 | - name: Run bandit 29 | run: bandit -c profile-bandit.yml ./django_admin_inline_paginator -r -ll -------------------------------------------------------------------------------- /.github/workflows/on-push.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality Tools 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | 9 | jobs: 10 | code-quality: 11 | uses: ./.github/workflows/code-quality.yml 12 | 13 | tests: 14 | uses: ./.github/workflows/tests.yml 15 | needs: [code-quality] 16 | 17 | # sonar: 18 | # uses: ./.github/workflows/sonar.yml 19 | # needs: [tests] 20 | # secrets: 21 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/on-tag.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | code-quality: 10 | uses: ./.github/workflows/code-quality.yml 11 | 12 | tests: 13 | uses: ./.github/workflows/tests.yml 14 | needs: [code-quality] 15 | 16 | # sonar: 17 | # uses: ./.github/workflows/sonar.yml 18 | # needs: [tests] 19 | # secrets: 20 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 21 | 22 | build-publish: 23 | uses: ./.github/workflows/pypi.yml 24 | needs: [tests] 25 | secrets: 26 | PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Build Package and Upload to PyPI 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | PYPI_API_TOKEN: 7 | required: true 8 | 9 | jobs: 10 | build-publish: 11 | name: Package to PyPI 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: "3.x" 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | 26 | - name: Build a binary wheel and a source tarball 27 | run: python3 setup.py sdist bdist_wheel --version=$GITHUB_REF_NAME 28 | 29 | - name: Publish distribution 📦 to PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | with: 32 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Run SonarQube 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SONAR_TOKEN: 7 | required: true 8 | 9 | jobs: 10 | sonar: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | # Disabling shallow clone is recommended for improving relevancy of reporting 16 | fetch-depth: 0 17 | 18 | - uses: actions/download-artifact@v2 19 | with: 20 | name: test-coverage 21 | 22 | - name: SonarCloud Scan 23 | uses: sonarsource/sonarcloud-github-action@master 24 | env: 25 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | tests: 8 | name: Tests 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | matrix: 12 | python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11'] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | pip install tox-gh-actions 23 | pip install ."[dev]" 24 | 25 | - name: Test with tox 26 | run: tox 27 | 28 | - uses: actions/upload-artifact@v2 29 | with: 30 | name: test-coverage 31 | path: coverage.xml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Folders 2 | /.idea/* 3 | /.vscode/* 4 | 5 | # Python Files 6 | *.pyc 7 | 8 | # SQL Files 9 | *.db 10 | *.sqlite3 11 | *.sql 12 | 13 | # Test's 14 | /htmlcov/* 15 | .coverage/ 16 | .coverage* 17 | coverage.xml 18 | pylint.txt 19 | /.tox/* 20 | 21 | # SonarQube 22 | .scannerwork 23 | 24 | # Others 25 | /venv/* 26 | /dist/* 27 | /django_admin_inline_paginator.egg-info/* -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore = migrations,tests,manage.py 3 | django-settings-module=config.settings 4 | load-plugins = 5 | pylint_common, 6 | pylint_django 7 | 8 | [MESSAGES CONTROL] 9 | disable= 10 | django-not-configured, 11 | missing-function-docstring, 12 | missing-class-docstring, 13 | missing-module-docstring, 14 | locally-disabled, 15 | too-few-public-methods 16 | 17 | [FORMAT] 18 | max-module-lines=1000 19 | max-line-length=120 20 | 21 | [DESIGN] 22 | max-args=10 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | 5 | ## [0.3.0] - 2021-12-26 6 | Support for type hint and compatibility with django 4.0 7 | 8 | * ### Added 9 | - [ISSUE-14](https://github.com/shinneider/django-admin-inline-paginator/issues/14) Add python type hint. 10 | - [ISSUE-18](https://github.com/shinneider/django-admin-inline-paginator/issues/18) Remove deprecated ugettext for support for django 4. 11 | 12 | * ### Changed 13 | * ### Fixed 14 | * ### Credits (Contributors) 15 | - [@aitorres](https://github.com/aitorres) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Shinneider 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 copies 9 | of the Software, and to permit persons to whom the Software is furnished to 10 | do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 17 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | 4 | recursive-include django_admin_inline_paginator/templates * 5 | recursive-include django_admin_inline_paginator/static * 6 | recursive-include django_admin_inline_paginator/locale * 7 | global-exclude __pycache__ 8 | global-exclude *.py[co] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Admin Inline Paginator 2 | ============================= 3 | 4 | The "Django Admin Inline Paginator" is simple way to paginate your inline in django admin 5 | 6 | If you use or like the project, click `Star` and `Watch` to generate metrics and i evaluate project continuity. 7 | 8 | # Install: 9 | 10 | ``` 11 | pip install django-admin-inline-paginator 12 | ``` 13 | 14 | # Usage: 15 | 16 | 1. Add to your INSTALLED_APPS, in settings.py: 17 | 18 | ``` 19 | INSTALLED_APPS = [ 20 | ... 21 | 'django_admin_inline_paginator', 22 | ... 23 | ] 24 | ``` 25 | 2. Create your model inline: 26 | 27 | ``` 28 | from django_admin_inline_paginator.admin import TabularInlinePaginated 29 | 30 | class ModelWithFKAdminInline(TabularInlinePaginated): 31 | fields = (...) 32 | per_page = 1 33 | model = ModelWithFK 34 | ``` 35 | 3. Create main model admin and use inline: 36 | 37 | ``` 38 | @register(YourModel) 39 | class YourModelAdmin(ModelAdmin): 40 | fields = (...) 41 | inlines = (ModelWithFKAdminInline, ) 42 | model = YourModel 43 | ``` 44 | 45 | # Advanced Usage: 46 | 47 | 1. Paginate multiples inlines: 48 | 49 | ``` 50 | class ModelWithFKInline(TabularInlinePaginated): 51 | fields = ('name', 'active') 52 | per_page = 2 53 | model = ModelWithFK 54 | pagination_key = 'page-model' # make sure it's unique for page inline 55 | 56 | class AnotherModelWithFKInline(TabularInlinePaginated): 57 | fields = ('name', 'active') 58 | per_page = 2 59 | model = AnotherModelWithFK 60 | pagination_key = 'page-another-model' # make sure it's unique for page inline 61 | ``` 62 | 63 | 2. Use previous inlines 64 | 65 | ``` 66 | @register(YourModel) 67 | class YourModelAdmin(ModelAdmin): 68 | fields = (...) 69 | inlines = (ModelWithFKAdminInline, AnotherModelWithFKInline) 70 | model = YourModel 71 | ``` 72 | 73 | # Images: 74 | 75 | ![image](https://user-images.githubusercontent.com/30196992/98023167-706ca880-1dfe-11eb-89fe-c056741f0d5b.png) 76 | 77 | # Need a Maintainer 78 | In the last months i don't have much time, health problemas, change of country and others problems. 79 | i have some surgeries for first part of 2022, and all of my current project don't use django-admin. 80 | for these reasons, i need a help for a project continuation!! 81 | -------------------------------------------------------------------------------- /django_admin_inline_paginator/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | import django 4 | 5 | if django.VERSION < (3, 2): # pragma: no cover 6 | default_app_config = 'django_admin_inline_paginator.apps.DjangoAdminInlinePaginatorConfig' 7 | except ImportError: 8 | pass 9 | -------------------------------------------------------------------------------- /django_admin_inline_paginator/admin.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from django.contrib.admin import 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 | can_show_all = True 16 | multi_page = True 17 | get_query_string = ChangeList.__dict__['get_query_string'] 18 | 19 | def __init__(self, request: HttpRequest, page_num: int, paginator: Paginator): 20 | self.show_all = 'all' in request.GET 21 | self.page_num = page_num 22 | self.paginator = paginator 23 | self.result_count = paginator.count 24 | self.params = dict(request.GET.items()) 25 | 26 | 27 | class PaginationFormSetBase: 28 | queryset: Optional[QuerySet] = None 29 | request: Optional[HttpRequest] = None 30 | per_page = 20 31 | pagination_key = 'page' 32 | 33 | def get_page_num(self) -> int: 34 | assert self.request is not None 35 | page = self.request.GET.get(self.pagination_key, '1') 36 | if page.isnumeric() and page > '0': 37 | return int(page) 38 | 39 | return 1 40 | 41 | def get_page(self, paginator: Paginator, page: int): 42 | if page <= paginator.num_pages: 43 | return paginator.page(page) 44 | 45 | return paginator.page(1) 46 | 47 | def mount_paginator(self, page_num: int = None): 48 | assert self.queryset is not None and self.request is not None 49 | page_num = self.get_page_num() if not page_num else page_num 50 | self.paginator = Paginator(self.queryset, self.per_page) 51 | self.page = self.get_page(self.paginator, page_num) 52 | self.cl = InlineChangeList(self.request, page_num, self.paginator) 53 | 54 | def mount_queryset(self): 55 | if self.cl.show_all: 56 | self._queryset = self.queryset 57 | 58 | self._queryset = self.page.object_list 59 | 60 | def __init__(self, *args, **kwargs): 61 | super(PaginationFormSetBase, self).__init__(*args, **kwargs) 62 | self.mount_paginator() 63 | self.mount_queryset() 64 | 65 | class InlinePaginated: 66 | pagination_key = 'page' 67 | template = 'admin/tabular_paginated.html' 68 | per_page = 20 69 | extra = 0 70 | 71 | def get_formset(self, request, obj=None, **kwargs): 72 | formset_class = super().get_formset(request, obj, **kwargs) 73 | 74 | class PaginationFormSet(PaginationFormSetBase, formset_class): 75 | pagination_key = self.pagination_key 76 | 77 | PaginationFormSet.request = request 78 | PaginationFormSet.per_page = self.per_page 79 | return PaginationFormSet 80 | 81 | class TabularInlinePaginated(InlinePaginated, TabularInline): 82 | pass 83 | 84 | class GenericTabularInlinePaginated(InlinePaginated, GenericTabularInline): 85 | pass 86 | -------------------------------------------------------------------------------- /django_admin_inline_paginator/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 DjangoAdminInlinePaginatorConfig(AppConfig): 7 | name = 'django_admin_inline_paginator' 8 | verbose_name = _('Django Admin Inline Paginator') 9 | -------------------------------------------------------------------------------- /django_admin_inline_paginator/static/django_admin_inline_paginator/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 | -------------------------------------------------------------------------------- /django_admin_inline_paginator/templates/admin/tabular_paginated.html: -------------------------------------------------------------------------------- 1 | {% include 'admin/edit_inline/tabular.html' %} 2 |
3 | {% include 'admin/tabular_paginator.html' %} 4 |
5 | -------------------------------------------------------------------------------- /django_admin_inline_paginator/templates/admin/tabular_paginator.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | {% load paginated_inline %} 3 | 4 | 5 | 6 |
7 | {% with inline_admin_formset.formset.page as page_obj %} 8 |

9 | {% if page_obj.has_previous %} 10 | {% trans 'previous' %} 11 | {% endif %} 12 | 13 | {% if page_obj.number|add:"-5" > 0 %} 14 | 1 15 | {% endif %} 16 | 17 | {% if page_obj.number|add:"-5" > 1 %} 18 | 19 | {% endif %} 20 | 21 | {% for page_num in page_obj.paginator.page_range %} 22 | {% if page_obj.number == page_num %} 23 | {{ page_num }} 24 | {% else %} 25 | {% if page_num > page_obj.number|add:"-5" and page_num < page_obj.number|add:"5" %} 26 | {{ page_num }} 27 | {% endif %} 28 | {% endif %} 29 | {% endfor %} 30 | 31 | {% if page_obj.number|add:"5" < page_obj.paginator.num_pages %} 32 | 33 | {% endif %} 34 | 35 | {% if page_obj.number|add:"4" < page_obj.paginator.num_pages %} 36 | {{ page_obj.paginator.num_pages }} 37 | {% endif %} 38 | 39 | {% if page_obj.has_next %} 40 | {% trans 'next' %} 41 | {% endif %} 42 | {{ page_obj.paginator.count }} Results 43 |

44 | {% endwith %} 45 |
46 | -------------------------------------------------------------------------------- /django_admin_inline_paginator/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinneider/django-admin-inline-paginator/4faa517abba80dd5116268e7a230742e553b4451/django_admin_inline_paginator/templatetags/__init__.py -------------------------------------------------------------------------------- /django_admin_inline_paginator/templatetags/paginated_inline.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | 3 | from django import template 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag 9 | def modify_pagination_path(full_path: str, key: str, value: str) -> str: 10 | get_params = full_path 11 | if get_params.find('?') > -1: 12 | get_params = get_params[get_params.find('?')+1:] 13 | if get_params.find('#') > -1: 14 | get_params = get_params[:get_params.find('#')] 15 | 16 | params = urllib.parse.parse_qs(get_params) 17 | params[key] = [str(value)] 18 | return urllib.parse.urlencode(params, doseq=True) 19 | -------------------------------------------------------------------------------- /example/app/example/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import ModelAdmin, register 2 | from django_admin_inline_paginator.admin import TabularInlinePaginated 3 | 4 | from .models import Country, Region, State 5 | 6 | 7 | class StateAdminInline(TabularInlinePaginated): 8 | fields = ('name', 'active') 9 | per_page = 5 10 | model = State 11 | 12 | 13 | class RegionAdminInline(TabularInlinePaginated): 14 | fields = ('name', 'active') 15 | per_page = 2 16 | model = Region 17 | pagination_key = 'rpage' 18 | 19 | 20 | @register(Country) 21 | class CountryAdmin(ModelAdmin): 22 | fields = ('name', 'active') 23 | inlines = (StateAdminInline, RegionAdminInline) 24 | model = Country 25 | -------------------------------------------------------------------------------- /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 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Country', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=100)), 20 | ('active', models.BooleanField(default=True)), 21 | ], 22 | options={ 23 | 'verbose_name': 'Country', 24 | 'verbose_name_plural': 'Countries', 25 | }, 26 | ), 27 | migrations.CreateModel( 28 | name='State', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('name', models.CharField(max_length=100)), 32 | ('active', models.BooleanField(default=True)), 33 | ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='example.Country')), 34 | ], 35 | options={ 36 | 'verbose_name': 'State', 37 | 'verbose_name_plural': 'States', 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='Region', 42 | fields=[ 43 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('name', models.CharField(max_length=100)), 45 | ('active', models.BooleanField(default=True)), 46 | ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='example.Country')), 47 | ], 48 | options={ 49 | 'verbose_name': 'Region', 50 | 'verbose_name_plural': 'Regions', 51 | }, 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /example/app/example/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinneider/django-admin-inline-paginator/4faa517abba80dd5116268e7a230742e553b4451/example/app/example/migrations/__init__.py -------------------------------------------------------------------------------- /example/app/example/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db.models import (CASCADE, BooleanField, CharField, ForeignKey, 3 | Model) 4 | 5 | 6 | class Country(Model): 7 | name = CharField(max_length=100) 8 | active = BooleanField(default=True) 9 | 10 | def __str__(self): 11 | return self.name 12 | 13 | class Meta: 14 | verbose_name = 'Country' 15 | verbose_name_plural = 'Countries' 16 | 17 | 18 | class State(Model): 19 | country = ForeignKey('example.Country', on_delete=CASCADE) 20 | name = CharField(max_length=100) 21 | active = BooleanField(default=True) 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | class Meta: 27 | verbose_name = 'State' 28 | verbose_name_plural = 'States' 29 | 30 | 31 | class Region(Model): 32 | country = ForeignKey('example.Country', on_delete=CASCADE) 33 | name = CharField(max_length=100) 34 | active = BooleanField(default=True) 35 | 36 | def __str__(self): 37 | return self.name 38 | 39 | class Meta: 40 | verbose_name = 'Region' 41 | verbose_name_plural = 'Regions' 42 | -------------------------------------------------------------------------------- /example/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinneider/django-admin-inline-paginator/4faa517abba80dd5116268e7a230742e553b4451/example/conf/__init__.py -------------------------------------------------------------------------------- /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 | 43 | # Third part apps 44 | 'django_admin_inline_paginator', 45 | 46 | # Developed apps 47 | 'app.example', 48 | ] 49 | 50 | MIDDLEWARE = [ 51 | 'django.middleware.security.SecurityMiddleware', 52 | 'django.contrib.sessions.middleware.SessionMiddleware', 53 | 'django.middleware.common.CommonMiddleware', 54 | 'django.middleware.csrf.CsrfViewMiddleware', 55 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'conf.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [ 66 | os.path.join(BASE_DIR, 'templates'), 67 | ], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'conf.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.sqlite3', 89 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 90 | } 91 | } 92 | 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | 127 | # Static files (CSS, JavaScript, Images) 128 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 129 | 130 | STATIC_URL = '/static/' 131 | -------------------------------------------------------------------------------- /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 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'conf.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | django==2.2.* 2 | .. 3 | -------------------------------------------------------------------------------- /profile-bandit.yml: -------------------------------------------------------------------------------- 1 | exclude_dirs: 2 | - "*/venv/*" 3 | - "*/.venv/*" 4 | - "*/env/*" 5 | - "*/.env/*" 6 | - "*/scripts/*" 7 | # skips: 8 | # - 'B102' 9 | # - 'B611' 10 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [pycodestyle] 5 | max-line-length = 120 6 | exclude = */.git/*,*/venv/*,*/migrations/*,.env 7 | 8 | [isort] 9 | sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 10 | known_django = django 11 | skip = venv,env,node_modules,migrations,.env,.venv 12 | skip_glob = */.git/*,*/venv/*,*/migrations/* 13 | line_length = 120 14 | 15 | [coverage:run] 16 | omit = 17 | */venv/* 18 | */env/* 19 | */.venv/* 20 | */.env/* 21 | **/__pycache__/** 22 | **/tests/** 23 | **/migrations/** 24 | config/** 25 | .vscode/** 26 | */.local/* 27 | */usr/lib/* -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from io import open 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | extras_require = { 8 | 'dev': [ 9 | 'django', 10 | 'django_mock_queries', 11 | 'pylint', 12 | 'pytest-pylint', 13 | 'pytest', 14 | 'pytest-cov', 15 | 'pytest-watch', 16 | 'tox' 17 | ], 18 | 'code-quality': [ 19 | 'isort', 20 | 'bandit', 21 | 'xenon', 22 | ], 23 | } 24 | 25 | 26 | def get_version(): 27 | version = '0.0' 28 | for arg in sys.argv: 29 | if arg.startswith('--version'): 30 | version = arg.split("=")[1] 31 | sys.argv.remove(arg) 32 | break 33 | 34 | return version if version[0] != 'v' else version[1:] 35 | 36 | 37 | setup( 38 | name='django-admin-inline-paginator', 39 | version=get_version(), 40 | description='The "Django Admin Inline Paginator" is simple way to paginate your inline in django admin', 41 | long_description=open('README.md', encoding='utf-8').read(), 42 | long_description_content_type='text/markdown', 43 | author='Shinneider Libanio da Silva', 44 | author_email='shinneider-libanio@hotmail.com', 45 | url='https://github.com/shinneider/django-admin-inline-paginator', 46 | license='MIT', 47 | packages=find_packages(exclude=['tests*']), 48 | include_package_data=True, 49 | python_requires=">=3.3", 50 | extras_require=extras_require, 51 | install_requires=[ 52 | 'django', 53 | ], 54 | classifiers=[ 55 | 'Development Status :: 4 - Beta', 56 | 'Environment :: Web Environment', 57 | 'Framework :: Django', 58 | 'Intended Audience :: Developers', 59 | 'License :: OSI Approved :: MIT License', 60 | 'Operating System :: OS Independent', 61 | 'Programming Language :: Python', 62 | 'Programming Language :: Python :: 3', 63 | 'Programming Language :: Python :: 3.5', 64 | 'Programming Language :: Python :: 3.6', 65 | 'Programming Language :: Python :: 3.7', 66 | 'Programming Language :: Python :: 3.8', 67 | 'Topic :: Internet :: WWW/HTTP', 68 | 'Topic :: Software Development :: Libraries :: Python Modules', 69 | ] 70 | ) 71 | -------------------------------------------------------------------------------- /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 | 17 | # Third part apps 18 | 'django_admin_inline_paginator', 19 | 'tests' 20 | ] 21 | 22 | settings.TEMPLATES = [ 23 | { 24 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 25 | 'DIRS': [ 26 | os.path.join(BASE_DIR, 'templates'), 27 | ], 28 | 'APP_DIRS': True, 29 | }, 30 | ] 31 | 32 | settings.STATIC_URL = '/' 33 | django.setup() 34 | -------------------------------------------------------------------------------- /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.admin import InlineChangeList, PaginationFormSetBase 6 | 7 | 8 | class TestInlineChangeList(unittest.TestCase): 9 | 10 | def test_default_values(self): 11 | self.assertEqual(InlineChangeList.can_show_all, True) 12 | self.assertEqual(InlineChangeList.multi_page, True) 13 | self.assertEqual(InlineChangeList.get_query_string, ChangeList.__dict__['get_query_string']) 14 | 15 | def test_init_values(self): 16 | pass 17 | # cl = InlineChangeList(request, page_num, paginator) 18 | # cl.page_num = page_num 19 | # cl.paginator = paginator 20 | # cl.result_count = paginator.count 21 | 22 | # cl.show_all = 'all' in request.GET 23 | # cl.params = dict(request.GET.items()) 24 | 25 | 26 | class TestPaginationFormSetBase(unittest.TestCase): 27 | 28 | def test_default_values(self): 29 | self.assertEqual(PaginationFormSetBase.queryset, None) 30 | self.assertEqual(PaginationFormSetBase.request, None) 31 | self.assertEqual(PaginationFormSetBase.per_page, 20) 32 | 33 | def test_get_page_num(self): 34 | pass 35 | 36 | def test_get_page(self): 37 | pass 38 | 39 | def test_mount_paginator(self): 40 | pass 41 | 42 | def test_mount_queryset(self): 43 | pass 44 | 45 | def test_init(self): 46 | pass 47 | -------------------------------------------------------------------------------- /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.apps import DjangoAdminInlinePaginatorConfig 7 | 8 | 9 | class TestDjangoAppConfig(unittest.TestCase): 10 | 11 | def test_valid_subclass_appconfig(self): 12 | self.assertEqual(issubclass(DjangoAdminInlinePaginatorConfig, AppConfig), True) 13 | 14 | def test_valid_name(self): 15 | name = DjangoAdminInlinePaginatorConfig.name 16 | self.assertEqual(isinstance(name, str), True) 17 | self.assertEqual(name, 'django_admin_inline_paginator') 18 | 19 | def test_valid_verbose_name(self): 20 | verbose_name = DjangoAdminInlinePaginatorConfig.verbose_name 21 | self.assertEqual(verbose_name, _('Django Admin Inline Paginator')) 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{36,37}-django{22,30,31,32} 3 | py{38,39}-django{22,30,31,32,40,41,42} 4 | py{310}-django{32,40,41,42} 5 | py{311}-django{41,42} 6 | 7 | [gh-actions] 8 | python = 9 | 3.6: py36 10 | 3.7: py37 11 | 3.8: py38 12 | 3.9: py39 13 | 3.10: py310 14 | 3.11: py311 15 | 16 | [testenv] 17 | commands = 18 | pytest --cov=django_admin_inline_paginator \ 19 | --cov-config=tox.ini \ 20 | --cov-fail-under=35 \ 21 | --cov-report=term-missing \ 22 | --cov-report=xml:coverage.xml \ 23 | --durations=10 \ 24 | --cov-append 25 | extras = dev 26 | deps = 27 | pytest 28 | pytest-cov 29 | django22: Django>=2.2,<2.3 30 | django30: Django>=3.0,<3.1 31 | django31: Django>=3.1,<3.2 32 | django32: Django>=3.2,<4.0 33 | django40: Django>=4.0,<4.1 34 | django41: Django>=4.1,<4.2 35 | django42: Django>=4.2,<5.0 36 | 37 | [coverage:run] 38 | relative_files = True 39 | source = django_admin_inline_paginator/ 40 | branch = True 41 | 42 | [testenv:report] 43 | deps = coverage 44 | skip_install = true 45 | commands = 46 | coverage report 47 | coverage html --------------------------------------------------------------------------------