diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5b8eeca..31acd9b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.4.3 +current_version = 2.5.0 parse = (?P\d+)\.(?P\d+)\.(?P\d+) serialize = {major}.{minor}.{release} commit = False @@ -9,3 +9,4 @@ allow_dirty = False tag_name = {new_version} [bumpversion:file:src/adminfilters/__init__.py] +[bumpversion:file:pyproject.toml] diff --git a/.github/file-filters.yml b/.github/file-filters.yml new file mode 100644 index 0000000..230e3a6 --- /dev/null +++ b/.github/file-filters.yml @@ -0,0 +1,30 @@ +# This is used by the action https://github.com/dorny/paths-filter +dependencies: &dependencies + - 'pyproject.toml' + +python: &python + - added|modified: 'src/**' + - added|modified: 'tests/**' + - 'manage.py' + +changelog: + - added|modified: 'changes/**' + - 'CHANGELOG.md' + +mypy: + - *python + - 'mypy.ini' + +run_tests: + - *python + - *dependencies + - 'pytest.ini' + - '.github/workflows/test.yml' + - '.github/file-filters.yml' + +lint: + - *python + - '.flake8' + - 'pyproject.toml' + - '.github/file-filters.yml' + - '.github/workflows/lint.yml' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..592fe4c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,67 @@ +name: "Documentation" + +on: + push: + branches: + - develop + - master + schedule: + - cron: '37 23 * * 2' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + generate: + name: Generate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Restore cached venv + id: cache-venv-restore + uses: actions/cache/restore@v4 + with: + path: | + .cache-uv/ + .venv/ + key: ${{ matrix.python-version }}-${{matrix.django-version}}-venv + + - uses: yezz123/setup-uv@v4 + - name: Build Doc + run: | + uv sync --extra docs + PYTHONPATH=./src uv run --cache-dir .cache-uv/ mkdocs build -d ./docs-output + + - name: Cache venv + if: steps.cache-venv-restore.outputs.cache-hit != 'true' + id: cache-venv-save + uses: actions/cache/save@v4 + with: + path: | + .cache-uv/ + .venv/ + key: ${{ matrix.python-version }}-${{matrix.django-version}}-venv + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./docs-output + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: generate + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..49f1873 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,62 @@ +name: Lint + +on: + push: + branches: + - '**' # matches every branch + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}-lint" + cancel-in-progress: true + +defaults: + run: + shell: bash + +permissions: + id-token: write + attestations: write + + +jobs: + changes: + runs-on: ubuntu-latest + timeout-minutes: 1 + defaults: + run: + shell: bash + outputs: + lint: ${{steps.changes.outputs.lint }} + steps: + - name: Checkout code + uses: actions/checkout@v4.1.7 + - id: changes + name: Check for file changes + uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0.0 + with: + base: ${{ github.ref }} + token: ${{ github.token }} + filters: .github/file-filters.yml + + lint: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + needs: [ changes ] + if: needs.changes.outputs.lint + steps: + - name: Checkout code + uses: actions/checkout@v4.1.7 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + architecture: 'x64' + - uses: yezz123/setup-uv@v4 + - name: lint + if: needs.changes.outputs.lint + run: | + uv run isort src/ --check-only + uv run flake8 src/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b45321..eb00cce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,30 +3,52 @@ name: Test on: push: branches: - - master - - develop - pull_request: + - '**' # matches every branch + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}-test" + cancel-in-progress: true + +defaults: + run: + shell: bash + +permissions: + id-token: write + attestations: write + jobs: - lint: + changes: runs-on: ubuntu-latest + timeout-minutes: 1 + defaults: + run: + shell: bash + outputs: + run_tests: ${{steps.changes.outputs.run_tests }} + lint: ${{steps.changes.outputs.lint }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip tox - - name: Lint with flake8 - run: | - tox -e lint + - name: Checkout code + uses: actions/checkout@v4.1.7 + - id: changes + name: Check for file changes + uses: dorny/paths-filter@v3.0.2 + with: + base: ${{ github.ref }} + token: ${{ github.token }} + filters: .github/file-filters.yml - test: - # if: ${{github.event}} && ${{ !contains(github.event.head_commit.message, 'ci skip') }} + ci: + needs: [ changes ] runs-on: ubuntu-latest + name: Test py${{ matrix.python-version }}/dj${{matrix.django-version}} + defaults: + run: + shell: bash services: postgres: - image: postgres:12 + image: postgres:14 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -35,30 +57,71 @@ jobs: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 strategy: - fail-fast: false + max-parallel: 1 matrix: - django-version: [ "3.2", "4.2", "5.0"] - python-version: [ "3.9", "3.10", "3.11", "3.12" ] - exclude: - - django-version: 5.0 - python-version: 3.9 + python-version: [ "3.11", "3.12" ] + django-version: [ "4.2", "5.1" ] + fail-fast: true +# if: needs.changes.outputs.run_tests || needs.changes.outputs.lint env: - DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/adminfilters - PY_VER: ${{ matrix.python-version}} - DJ_VER: ${{ matrix.django-version}} + DOCKER_DEFAULT_PLATFORM: linux/amd64 + DATABASE_URL: postgres://postgres:postgres@localhost:5432/adminfilters steps: - - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + architecture: 'x64' + - name: Restore cached venv + id: cache-venv-restore + uses: actions/cache/restore@v4 + with: + path: | + .cache-uv/ + .venv/ + key: ${{ matrix.python-version }}-${{matrix.django-version}}-${{ hashFiles('pyproject.toml') }}-venv - - name: Install dependencies - run: python -m pip install --upgrade pip .[test] "django==${DJ_VER}.*" + - uses: yezz123/setup-uv@v4 + with: + python: ${{ matrix.python-version }} - - name: Test with - run: py.test --selenium -vv --cov-report=xml --cov-report=term --junitxml=pytest.xml --cov-config=tests/.coveragerc --cov adminfilters + - name: Test +# if: needs.changes.outputs.run_tests + run: | + uv export -q --no-hashes -o requirements.txt + pip install -r requirements.txt + pip install "django==${{ matrix.django-version }}.*" + python -m pytest tests/ \ + --junit-xml junit-${{ matrix.python-version }}-${{matrix.django-version}}.xml \ + --cov --cov-report xml + - name: Cache venv + if: steps.cache-venv-restore.outputs.cache-hit != 'true' + id: cache-venv-save + uses: actions/cache/save@v4 + with: + path: | + .cache-uv/ + .venv/ + key: ${{ matrix.python-version }}-${{matrix.django-version}}-${{ hashFiles('pyproject.toml') }}-venv + + - name: Upload pytest test results + uses: actions/upload-artifact@v4 + with: + name: pytest-results-${{ matrix.python-version }}-${{matrix.django-version}} + path: junit-${{ matrix.python-version }}-${{matrix.django-version}}.xml + if: ${{ always() }} - - uses: codecov/codecov-action@v1 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + if: matrix.python-version == 3.12 + continue-on-error: true with: - verbose: true # optional (default = false) + env_vars: OS,PYTHON + fail_ci_if_error: true + flags: unittests + files: ./coverage.xml + verbose: false + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-${{env.GITHUB_REF_NAME}} diff --git a/.gitignore b/.gitignore index 24e26cf..6c3f5e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ .* ~* +*.lock build __pycache__ *.egg-info +!docs/**/.pages !.gitignore !.github !.bumpversion.cfg diff --git a/CHANGES.md b/CHANGES.md index 50bc442..823d080 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Release 2.5 +=========== +* PEP 621 support (pyproject.toml) +* Added partial Proxy Models support to AutoCompleteFilter + + Release 2.4.3 ============= * Fixes bug in UnionListFilter @@ -12,7 +18,7 @@ Release 2.4.2 Release 2.4.1 ============= -* Fixes bug in AutoCompleteFilter +* Fixes bug in AutiCompleteFilter Release 2.4 diff --git a/README.md b/README.md index e977061..8713170 100644 --- a/README.md +++ b/README.md @@ -59,39 +59,41 @@ When you use `FilterDepotManager` to save a filter, the call is *idempotent* but Usage examples ============== - - class MyModel(models.Model): - index = models.CharField(max_length=255) - name = models.CharField(max_length=255) - age = models.IntegerField() - flag = models.CharField(default="1", choices=(("0", "Flag 1"), ("1", "Flag 2")) - household = models.ForeignKey('Household') - custom = JSONField(default=dict, blank=True) - - class MyModelAdmin(ModelAdmin): - list_filter = ( - FilterDepotManager, # needs `adminfilters.depot` app - QueryStringFilter, - DjangoLookupFilter, - ("custom", JsonFieldFilter.factory(can_negate=False, options=True)), - ("flag", ChoicesFieldComboFilter), - ('household', AutoCompleteFilter) - ('name', ValueFilter.factory(lookup='istartswith'), - ("age", NumberFilter), - ) - +```python +class MyModel(models.Model): + index = models.CharField(max_length=255) + name = models.CharField(max_length=255) + age = models.IntegerField() + flag = models.CharField(default="1", choices=(("0", "Flag 1"), ("1", "Flag 2")) + household = models.ForeignKey("Household") + custom = JSONField(default=dict, blank=True) + + +class MyModelAdmin(ModelAdmin): + list_filter = ( + FilterDepotManager, # needs `adminfilters.depot` app + QueryStringFilter, + DjangoLookupFilter, + ("custom", JsonFieldFilter.factory(can_negate=False, options=True)), + ("flag", ChoicesFieldComboFilter), + ("household", AutoCompleteFilter) + ("name", ValueFilter.factory(lookup="istartswith"), + ("age", NumberFilter), + ) +``` Run demo app ============ - $ git clone https://github.com/saxix/django-adminfilters.git - $ cd django-adminfilters - $ python3 -m venv .venv - $ source .venv/bin/activate - $ make develop - $ make demo - +```sh +git clone https://github.com/saxix/django-adminfilters.git +cd django-adminfilters +python3 -m venv .venv +source .venv/bin/activate +make develop +make demo +``` Project links ------------- diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py deleted file mode 100644 index e16d611..0000000 --- a/docs/_ext/djangodocs.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -Sphinx plugins for Django documentation. -""" -import json -import os -import re - -from sphinx import __version__ as sphinx_ver -from sphinx import addnodes -from sphinx.builders.html import StandaloneHTMLBuilder -from sphinx.util.compat import Directive -from sphinx.util.console import bold -from sphinx.writers.html import SmartyPantsHTMLTranslator - -# RE for option descriptions without a '--' prefix -simple_option_desc_re = re.compile(r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') - - -def setup(app): - app.add_crossref_type( - directivename='setting', - rolename='setting', - indextemplate='pair: %s; setting', - ) - app.add_crossref_type( - directivename='templatetag', - rolename='ttag', - indextemplate='pair: %s; template tag', - ) - app.add_crossref_type( - directivename='templatefilter', - rolename='tfilter', - indextemplate='pair: %s; template filter', - ) - app.add_crossref_type( - directivename='fieldlookup', - rolename='lookup', - indextemplate='pair: %s; field lookup type', - ) - app.add_description_unit( - directivename='django-admin', - rolename='djadmin', - indextemplate='pair: %s; django-admin command', - parse_node=parse_django_admin_node, - ) - app.add_description_unit( - directivename='django-admin-option', - rolename='djadminopt', - indextemplate='pair: %s; django-admin command-line option', - parse_node=parse_django_adminopt_node, - ) - # app.add_config_value('django_next_version', '0.0', True) - # app.add_directive('versionadded', VersionDirective) - # app.add_directive('versionchanged', VersionDirective) - app.add_builder(DjangoStandaloneHTMLBuilder) - - -class VersionDirective(Directive): - has_content = True - required_arguments = 1 - optional_arguments = 1 - final_argument_whitespace = True - option_spec = {} - - def run(self): - env = self.state.document.settings.env - ret = [] - node = addnodes.versionmodified() - ret.append(node) - if self.arguments[0] == env.config.django_next_version: - node['version'] = 'Development version' - else: - node['version'] = self.arguments[0] - node['type'] = self.name - if len(self.arguments) == 2: - inodes, messages = self.state.inline_text( - self.arguments[1], self.lineno + 1 - ) - node.extend(inodes) - if self.content: - self.state.nested_parse(self.content, self.content_offset, node) - ret = ret + messages - env.note_versionchange(node['type'], node['version'], node, self.lineno) - return ret - - -class DjangoHTMLTranslator(SmartyPantsHTMLTranslator): - """ - Django-specific reST to HTML tweaks. - """ - - # Don't use border=1, which docutils does by default. - def visit_table(self, node): - self._table_row_index = 0 # Needed by Sphinx - self.body.append(self.starttag(node, 'table', CLASS='docutils')) - - # ? Really? - def visit_desc_parameterlist(self, node): - self.body.append('(') - self.first_param = 1 - self.param_separator = node.child_text_separator - - def depart_desc_parameterlist(self, node): - self.body.append(')') - - if sphinx_ver < '1.0.8': - # - # Don't apply smartypants to literal blocks - # - def visit_literal_block(self, node): - self.no_smarty += 1 - SmartyPantsHTMLTranslator.visit_literal_block(self, node) - - def depart_literal_block(self, node): - SmartyPantsHTMLTranslator.depart_literal_block(self, node) - self.no_smarty -= 1 - - # - # Turn the "new in version" stuff (versionadded/versionchanged) into a - # better callout -- the Sphinx default is just a little span, - # which is a bit less obvious that I'd like. - # - # FIXME: these messages are all hardcoded in English. We need to change - # that to accomodate other language docs, but I can't work out how to make - # that work. - # - version_text = { - 'deprecated': 'Deprecated in Django %s', - 'versionchanged': 'Changed in Django %s', - 'versionadded': 'New in Django %s', - } - - def visit_versionmodified(self, node): - self.body.append(self.starttag(node, 'div', CLASS=node['type'])) - title = '%s%s' % ( - self.version_text[node['type']] % node['version'], - len(node) and ':' or '.', - ) - self.body.append('%s ' % title) - - def depart_versionmodified(self, node): - self.body.append('\n') - - # Give each section a unique ID -- nice for custom CSS hooks - def visit_section(self, node): - old_ids = node.get('ids', []) - node['ids'] = ['s-' + i for i in old_ids] - node['ids'].extend(old_ids) - SmartyPantsHTMLTranslator.visit_section(self, node) - node['ids'] = old_ids - - -def parse_django_admin_node(env, sig, signode): - command = sig.split(' ')[0] - env._django_curr_admin_command = command - title = 'django-admin.py %s' % sig - signode += addnodes.desc_name(title, title) - return sig - - -def parse_django_adminopt_node(env, sig, signode): - """A copy of sphinx.directives.CmdoptionDesc.parse_signature()""" - from sphinx.domains.std import option_desc_re - - count = 0 - firstname = '' - for m in option_desc_re.finditer(sig): - optname, args = m.groups() - if count: - signode += addnodes.desc_addname(', ', ', ') - signode += addnodes.desc_name(optname, optname) - signode += addnodes.desc_addname(args, args) - if not count: - firstname = optname - count += 1 - if not count: - for m in simple_option_desc_re.finditer(sig): - optname, args = m.groups() - if count: - signode += addnodes.desc_addname(', ', ', ') - signode += addnodes.desc_name(optname, optname) - signode += addnodes.desc_addname(args, args) - if not count: - firstname = optname - count += 1 - if not firstname: - raise ValueError - return firstname - - -class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder): - """ - Subclass to add some extra things we need. - """ - - name = 'djangohtml' - - def finish(self): - super().finish() - self.info(bold('writing templatebuiltins.js...')) - xrefs = self.env.domaindata['std']['objects'] - templatebuiltins = { - 'ttags': [ - n - for ((t, n), (lnk, a)) in xrefs.items() - if t == 'templatetag' and lnk == 'ref/templates/builtins' - ], - 'tfilters': [ - n - for ((t, n), (lnk, a)) in xrefs.items() - if t == 'templatefilter' and lnk == 'ref/templates/builtins' - ], - } - outfilename = os.path.join(self.outdir, 'templatebuiltins.js') - with open(outfilename, 'wb') as fp: - fp.write('var django_template_builtins = ') - json.dump(templatebuiltins, fp) - fp.write(';\n') diff --git a/docs/_ext/github.py b/docs/_ext/github.py deleted file mode 100644 index 7cfefb4..0000000 --- a/docs/_ext/github.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Define text roles for GitHub - -* ghissue - Issue -* ghpull - Pull Request -* ghuser - User - -Adapted from bitbucket example here: -https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py - -Authors -------- - -* Doug Hellmann -* Min RK -""" -# -# Original Copyright (c) 2010 Doug Hellmann. All rights reserved. -# - -from docutils import nodes, utils -from docutils.parsers.rst.roles import set_classes - - -def make_link_node(rawtext, app, type, slug, options): - """Create a link to a github resource. - - :param rawtext: Text being replaced with link node. - :param app: Sphinx application context - :param type: Link type (issues, changeset, etc.) - :param slug: ID of the thing to link to - :param options: Options dictionary passed to role func. - """ - - try: - base = app.config.github_project_url - if not base: - raise AttributeError - if not base.endswith('/'): - base += '/' - except AttributeError as err: - raise ValueError( - 'github_project_url configuration value is not set (%s)' % str(err) - ) - - ref = base + type + '/' + slug + '/' - set_classes(options) - prefix = '#' - if type == 'pull': - prefix = 'PR ' + prefix - node = nodes.reference( - rawtext, prefix + utils.unescape(slug), refuri=ref, **options - ) - return node - - -def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub issue. - - Returns 2 part tuple containing list of nodes to insert into the - document and a list of system messages. Both are allowed to be - empty. - - :param name: The role name used in the document. - :param rawtext: The entire markup snippet, with role. - :param text: The text marked with the role. - :param lineno: The line number where rawtext appears in the input. - :param inliner: The inliner instance that called us. - :param options: Directive options for customization. - :param content: The directive content for customization. - """ - - try: - issue_num = int(text) - if issue_num <= 0: - raise ValueError - except ValueError: - msg = inliner.reporter.error( - 'GitHub issue number must be a number greater than or equal to 1; ' - '"%s" is invalid.' % text, - line=lineno, - ) - prb = inliner.problematic(rawtext, rawtext, msg) - return [prb], [msg] - app = inliner.document.settings.env.app - if 'pull' in name.lower(): - category = 'pull' - elif 'issue' in name.lower(): - category = 'issues' - else: - msg = inliner.reporter.error( - 'GitHub roles include "ghpull" and "ghissue", ' '"%s" is invalid.' % name, - line=lineno, - ) - prb = inliner.problematic(rawtext, rawtext, msg) - return [prb], [msg] - node = make_link_node(rawtext, app, category, str(issue_num), options) - return [node], [] - - -def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub user. - - Returns 2 part tuple containing list of nodes to insert into the - document and a list of system messages. Both are allowed to be - empty. - - :param name: The role name used in the document. - :param rawtext: The entire markup snippet, with role. - :param text: The text marked with the role. - :param lineno: The line number where rawtext appears in the input. - :param inliner: The inliner instance that called us. - :param options: Directive options for customization. - :param content: The directive content for customization. - """ - # app = inliner.document.settings.env.app - ref = 'https://www.github.com/' + text - node = nodes.reference(rawtext, text, refuri=ref, **options) - return [node], [] - - -def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub commit. - - Returns 2 part tuple containing list of nodes to insert into the - document and a list of system messages. Both are allowed to be - empty. - - :param name: The role name used in the document. - :param rawtext: The entire markup snippet, with role. - :param text: The text marked with the role. - :param lineno: The line number where rawtext appears in the input. - :param inliner: The inliner instance that called us. - :param options: Directive options for customization. - :param content: The directive content for customization. - """ - app = inliner.document.settings.env.app - try: - base = app.config.github_project_url - if not base: - raise AttributeError - if not base.endswith('/'): - base += '/' - except AttributeError as err: - raise ValueError( - 'github_project_url configuration value is not set (%s)' % str(err) - ) - - ref = base + text - node = nodes.reference(rawtext, text[:6], refuri=ref, **options) - return [node], [] - - -def setup(app): - """Install the plugin. - - :param app: Sphinx application context. - """ - app.add_role('ghissue', ghissue_role) - app.add_role('ghpull', ghissue_role) - app.add_role('ghuser', ghuser_role) - app.add_role('ghcommit', ghcommit_role) - app.add_config_value('github_project_url', None, 'env') - return diff --git a/docs/_ext/version.py b/docs/_ext/version.py deleted file mode 100644 index 0383e53..0000000 --- a/docs/_ext/version.py +++ /dev/null @@ -1,75 +0,0 @@ -import re - -from sphinx import addnodes, roles -from sphinx.util.compat import Directive - -simple_option_desc_re = re.compile(r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') - - -def setup(app): - app.add_crossref_type( - directivename='setting', - rolename='setting', - indextemplate='pair: %s; setting', - ) - app.add_crossref_type( - directivename='templatetag', - rolename='ttag', - indextemplate='pair: %s; template tag', - ) - app.add_crossref_type( - directivename='templatefilter', - rolename='tfilter', - indextemplate='pair: %s; template filter', - ) - app.add_crossref_type( - directivename='fieldlookup', - rolename='lookup', - indextemplate='pair: %s; field lookup type', - ) - app.add_config_value('next_version', '0.0', True) - app.add_directive('versionadded', VersionDirective) - app.add_directive('versionchanged', VersionDirective) - app.add_crossref_type( - directivename='release', - rolename='release', - indextemplate='pair: %s; release', - ) - - -class VersionDirective(Directive): - has_content = True - required_arguments = 1 - optional_arguments = 1 - final_argument_whitespace = True - option_spec = {} - version_text = { - 'deprecated': 'Deprecated in %s.', - 'versionchanged': 'Changed in %s.', - 'versionadded': 'New in %s.', - } - - def run(self): - env = self.state.document.settings.env - arg0 = self.arguments[0] - ret = [] - node = addnodes.versionmodified() - ret.append(node) - - node['type'] = self.name - if env.config.next_version == arg0: - version = 'Development version' - link = None - else: - version = arg0 - link = 'release-%s' % arg0 - - node['version'] = version - # inodes, messages = self.state.inline_text(self.version_text[self.name] % version, self.lineno+1) - # node.extend(inodes) - if link: - text = ' Please see the changelog <%s>' % link - xrefs = roles.XRefRole()('std:ref', text, text, self.lineno, self.state) - node.extend(xrefs[0]) - env.note_versionchange(node['type'], node['version'], node, self.lineno) - return ret diff --git a/docs/to_gif.sh b/docs/bin/to_gif.sh similarity index 100% rename from docs/to_gif.sh rename to docs/bin/to_gif.sh diff --git a/docs/changes.rst b/docs/changes.rst deleted file mode 100644 index 15b3ed7..0000000 --- a/docs/changes.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _changes: - -:tocdepth: 2 - -========== -Changelog -========== - -This sections lists the biggest changes done on each release. - - -.. include:: ../CHANGES.md diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index dd93d10..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,271 +0,0 @@ -# Django site maintenance documentation build configuration file, created by -# sphinx-quickstart on Sun Dec 5 19:11:46 2010. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import os -import sys - -from django.conf import settings - -import adminfilters as app - -here = os.path.abspath(os.path.join(os.path.dirname(__file__))) -up = lambda base, level: os.path.abspath(os.path.join(base, *([os.pardir] * level))) -sys.path.insert(0, up(here, 2)) - -settings.configure(SITE_ID=1) - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ----------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '_ext'))) -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.todo', - 'sphinx.ext.graphviz', - 'sphinx.ext.intersphinx', - 'sphinx.ext.doctest', - 'sphinx.ext.extlinks', - 'sphinx.ext.autosummary', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinxcontrib.video', - 'github', -] - -next_version = 'dev' -github_project_url = 'https://github.com/saxix/django-adminfilters/' -github_project_url_basesource = 'https://github.com/saxix/django-adminfilters/' - -todo_include_todos = True -intersphinx_mapping = { - 'python': ('http://python.readthedocs.org/en/latest/', None), - 'django': ('http://django.readthedocs.org/en/1.7.x/', None), - # 'sphinx': ('http://sphinx.readthedocs.org/en/latest/', None) -} -intersphinx_cache_limit = 90 # days - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'Django AdminFilters' -copyright = '2012-2024, Stefano Apostolico' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = app.VERSION -# The full version, including alpha/beta/rc tags. -release = app.VERSION - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# if os.environ.get('READTHEDOCS', None) == 'True': -# html_theme = 'default' -# else: -# import sphinx_rtd_theme -# -# html_theme = 'sphinx_rtd_theme' -# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -# -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['.'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -html_use_smartypants = True - -# HTML translator class for the builder -# html_translator_class = "version.HTMLTranslator" - -# Content template for the index page. -# html_index = '' - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'djangoadminactionsdoc' - -# -- Options for LaTeX output -------------------------------------------- - -# The paper size ('letter' or 'a4'). -# latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -# latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ( - 'index', - 'DjangoAdminActions.tex', - 'Django Admin Actions Documentation', - 'Stefano Apostolico', - 'manual', - ), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Additional stuff for the LaTeX preamble. -# latex_preamble = '' - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ( - 'index', - 'djangoadminactions', - 'Django Admin Actions Documentation', - ['Stefano Apostolico'], - 1, - ) -] diff --git a/docs/depot/_index.rst b/docs/depot/_index.rst deleted file mode 100644 index 105940d..0000000 --- a/docs/depot/_index.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. include:: globals.rst -.. _depot: - -===== -Depot -===== - -.. versionadded:: 2.0 - -.. image:: ../images/depot.gif - :width: 800 - - -This optional django app allows you to store/retrieve filters configuration. -It is fully compatible with any filter - - -Install -------- - -Just as any other Django application - -.. code-block:: python - :caption: settings.py - - # settings.py - INSTALLED_APPS = ( - ... - 'django.contrib.admin', - 'adminfilters.depot', - ) - - -.. code-block:: python - :caption: admin.py - - # admin.py - from adminfilters.depot.widget import DepotManager - from adminfilters.mixin import AdminFiltersMixin - - class ArtistModelAdmin(AdminFiltersMixin, ModelAdmin): - list_filter = ( - DepotManager, - ... - ) - -Now you are able to store/retrieve filtering criteria for ``ArtistModelAdmin``. -Filters are stored in :class:StoredFilter model and availble thru ``StoredFilterAdmin`` - - - -.. _depot_widget: - - -DepotManager ------------- - -.. image:: ../images/depot.png - :width: 200 diff --git a/docs/filters/_index.rst b/docs/filters/_index.rst deleted file mode 100644 index 91e657a..0000000 --- a/docs/filters/_index.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. include:: ../globals.rst - -.. _filters: - -======= -Filters -======= - - -.. toctree:: - :maxdepth: 1 - - autocomplete - json - value - multivalue - date - dj - querystring - multiselect - numbers - combo - radio - extra diff --git a/docs/filters/combo.rst b/docs/filters/combo.rst deleted file mode 100644 index c1c4e23..0000000 --- a/docs/filters/combo.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. include:: ../globals.rst - -.. _filters_combo: - - -.. image:: ../images/choices_field_combo.gif - :width: 900 - - -==================== -AllValuesComboFilter -==================== - - Just overrides standard template of Django ``AllValuesFieldListFilter`` to use Combobox widget - - -======================= -ChoicesFieldComboFilter -======================= - - Just overrides standard template of Django ``ChoicesFieldListFilter`` to use Combobox widget - - -======================= -RelatedFieldComboFilter -======================= - - Just overrides standard template of Django ``RelatedFieldListFilter`` to use Combobox widget diff --git a/docs/filters/date.rst b/docs/filters/date.rst deleted file mode 100644 index e8ac5c0..0000000 --- a/docs/filters/date.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. include:: ../globals.rst - -.. _filters_number: - -=============== -DateRangeFilter -=============== - -Filter dates. It allows complex filter like: - - -.. list-table:: Operators - :widths: 30 70 - - - * - 2000-01-01 - - equals 2000-01-01 - * - =2000-01-01 - - equals 2000-01-01 - * - > 2000-01-03 - - greater than 2000-01-03 - * - < 2000-01-03 - - lower than 2000-01-03 - * - >= 2000-01-03 - - greater or equal than 2000-01-03 - * - >= 2000-01-03 - - lower or equal than 2000-01-03 - * - 2000-01-02..2000-12-02 - - betwenen 2000-01-02 and 2000-12-02 - * - <> 2000-01-02 - - not equal to 2000-01-02 - * - 2000-01-01,2000-02-01,2000-03-01 - - list of values diff --git a/docs/filters/extra.rst b/docs/filters/extra.rst deleted file mode 100644 index c424671..0000000 --- a/docs/filters/extra.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. include:: ../globals.rst - -.. _filters_extra: - - -====================== -PermissionPrefixFilter -====================== diff --git a/docs/filters/multiselect.rst b/docs/filters/multiselect.rst deleted file mode 100644 index abb379f..0000000 --- a/docs/filters/multiselect.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. include:: ../globals.rst - -.. _filters_multiselect: - - -=========================== -IntersectionFieldListFilter -=========================== - -A FieldListFilter which allows multiple selection of filters for many-to-many type fields. A list of objects will be returned whose m2m contains all the selected filters. - -.. image:: ../images/intersection.gif - :width: 900 - - -Usage ------ - -python:: - - - class DemoModelAdmin_UnionFieldListFilter(DebugMixin, ModelAdmin): - list_filter = (("bands", IntersectionFieldListFilter),) - - - - -==================== -UnionFieldListFilter -==================== - - -.. image:: ../images/union.gif - :width: 900 - -A FieldListFilter which allows multiple selection of filters for many-to-many type fields, or any type with choices. A list of objects will be returned whose m2m or value set contains one of the selected filters. - - -Usage ------ - -python:: - - - class DemoModelAdmin_UnionFieldListFilter(DebugMixin, ModelAdmin): - list_filter = (("bands", UnionFieldListFilter),) diff --git a/docs/filters/numbers.rst b/docs/filters/numbers.rst deleted file mode 100644 index e5eb6e8..0000000 --- a/docs/filters/numbers.rst +++ /dev/null @@ -1,32 +0,0 @@ -.. include:: ../globals.rst - -.. _filters_number: - -============ -NumberFilter -============ - -Filter numbers. It allows complex filter like: - - -.. list-table:: Operators - :widths: 30 70 - - * - 100 - - equals 100 - * - =100 - - equals 100 - * - > 100 - - greater than 100 - * - < 100 - - lower than 100 - * - >= 100 - - greater or equal than 100 - * - >= 100 - - lower or equal than 100 - * - 100..200 - - betwenen 100 and 200 - * - <> 100 - - not equal to 100 - * - 1,4,5 - - list of numbers diff --git a/docs/filters/radio.rst b/docs/filters/radio.rst deleted file mode 100644 index 0212200..0000000 --- a/docs/filters/radio.rst +++ /dev/null @@ -1,26 +0,0 @@ -.. include:: ../globals.rst - -.. _filters_radio: - - - -.. image:: ../images/boolean_radio.gif - :width: 900 - - -==================== -AllValuesRadioFilter -==================== - -================== -BooleanRadioFilter -================== - - -======================= -ChoicesFieldRadioFilter -======================= - -======================= -RelatedFieldRadioFilter -======================= diff --git a/docs/genindex.rst b/docs/genindex.rst deleted file mode 100644 index 9e530fa..0000000 --- a/docs/genindex.rst +++ /dev/null @@ -1,2 +0,0 @@ -Index -===== diff --git a/docs/globals.rst b/docs/globals.rst deleted file mode 100644 index eb32578..0000000 --- a/docs/globals.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _GitHub: http://github.com/saxix/django-adminfilters -.. _pip: http://pip.openplans.org/ -.. _PyPI: http://pypi.python.org/ -.. _virtualenv: http://virtualenv.openplans.org/ - -.. |app| replace:: django-adminfilters - - -.. _app: https://github.com/saxix/django-adminfilters -.. _django_admin: https://docs.djangoproject.com/en/dev/ref/contrib/admin/ -.. _select2: https://select2.org/ diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 8182b0a..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,58 +0,0 @@ -.. include:: globals.rst -.. _index: - -=================== -django-adminfilters -=================== - -.. image:: https://badge.fury.io/py/django-adminfilters.svg - :alt: Project Home - :target: https://pypi.org/project/django-adminfilters/ - -.. image:: https://github.com/saxix/django-adminfilters/actions/workflows/test.yml/badge.svg - :alt: CI Status - :target: https://github.com/saxix/django-adminfilters/actions/workflows/test.yml - -.. image:: https://codecov.io/github/saxix/django-adminfilters/coverage.svg?branch=develop - :alt: Coverage - :target: https://codecov.io/github/saxix/django-adminfilters?branch=develop - -.. image:: https://readthedocs.org/projects/django-adminfilters/badge/?version=latest - :alt: Documentation Status - :target: https://django-adminfilters.readthedocs.io/en/latest/ - -About -===== - -Collection of useful filters to use with django admin site with low impact and no dependencies with external libraries. - - - -Sample application is available at https://django-adminfilters.herokuapp.com/ - - -Table Of Contents -================= - -.. toctree:: - :maxdepth: 1 - - install - filters/_index - depot/_index - changes - - -Links -===== - - * Project home page: https://github.com/saxix/django-adminfilters - * Issue tracker: https://github.com/saxix/django-adminfilters/issues?sort - * Download: http://pypi.python.org/pypi/django-adminfilters/ - * Documentation: http://readthedocs.org/docs/django-adminfilters/en/latest/ - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`search` diff --git a/docs/install.rst b/docs/install.rst deleted file mode 100644 index c6ac311..0000000 --- a/docs/install.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. include:: globals.rst - -.. _install: - -============ -Installation -============ - -Installing django-adminfilters is as simple as checking out the source and adding it to -your project or ``PYTHONPATH``. - - -1. First of all follow the instruction to install `django_admin`_ application, - -2. Either check out django-adminfilters from `GitHub`_ or to pull a release off `PyPI`_. Doing ``pip install django-adminfilters`` or ``easy_install django-adminfilters`` is all that should be required. - -3. Either symlink the ``adminfilters`` directory into your project or copy the directory in. What ever works best for you. - - -Install test dependencies -========================= - -If you want to run :mod:`adminfilters` tests you need extra requirements - - -.. code-block:: python - - pip install -U django-adminfilters - - -Configuration -============= - -Add :mod:`adminfilters` to your `INSTALLED_APPS`:: - - INSTALLED_APPS = ( - 'adminfilters', - ... - ) diff --git a/docs/src/.pages b/docs/src/.pages new file mode 100644 index 0000000..e69de29 diff --git a/docs/src/contributing.md b/docs/src/contributing.md new file mode 100644 index 0000000..b2343b7 --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1,27 @@ +# Contributing + + +Install [uv](https://docs.astral.sh/uv/) + + + git clone .. + uv venv .venv --python 3.12 + source .venv/bin/activate + uv sync --extra docs + pre-commit install + + + +## Run tests + pytests tests + + +## Run demo app + + tests/demoapp/manage.py migrate + + tests/demoapp/manage.py runserver + +!!! note + + You can login in the demo application as superuser using any username/password diff --git a/docs/src/filters/.pages b/docs/src/filters/.pages new file mode 100644 index 0000000..e69de29 diff --git a/docs/filters/autocomplete.rst b/docs/src/filters/autocomplete.md similarity index 73% rename from docs/filters/autocomplete.rst rename to docs/src/filters/autocomplete.md index 072dfb7..ae26bf3 100644 --- a/docs/filters/autocomplete.rst +++ b/docs/src/filters/autocomplete.md @@ -1,25 +1,18 @@ -.. include:: ../globals.rst +# Autocomplete Filters -.. _filters_autocomplete: +## AutoComplete - -Autocomplete -============ - -.. image:: ../images/autocomplete.gif - :width: 200 +![autocomplete](../static/images/autocomplete.gif){width=200} This filter is for ForeignKeys and uses select2_ javascript. It is based on the standard Django -autocomplete_ implementation, no external libraries are needed. +[autocomplete](https://docs.djangoproject.com/en/4.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields) implementation, no external libraries are needed. See Django autocomplete_ documentation for the ajax service options. -Usage ------ +### Usage -python:: class MyCountry(models.ModelAdmin): search_fields = ('name', ) @@ -33,24 +26,16 @@ python:: -.. _autocomplete: https://docs.djangoproject.com/en/4.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields - +## LinkedAutoComplete +![autocomplete](../static/images/autocomplete.gif){width=200,align=center} -LinkedAutoComplete -================== - -.. image:: ../images/autocomplete.gif - :width: 200 As filter_autocomplete_ it can be used in case dependant master/details elements where we want to limits the "details" based on the "master" selection. -Usage ------ - -python:: +### Usage class Country(models.Model): ... diff --git a/docs/src/filters/combo.md b/docs/src/filters/combo.md new file mode 100644 index 0000000..eb04c1d --- /dev/null +++ b/docs/src/filters/combo.md @@ -0,0 +1,18 @@ +# ComboBox Filters + +![combo](../static/images/choices_field_combo.gif){width=300} + + +## AllValuesComboFilter + +Just overrides standard template of Django ``AllValuesFieldListFilter`` to use Combobox widget + + +## ChoicesFieldComboFilter + +Just overrides standard template of Django ``ChoicesFieldListFilter`` to use Combobox widget + + +## RelatedFieldComboFilter + +Just overrides standard template of Django ``RelatedFieldListFilter`` to use Combobox widget diff --git a/docs/src/filters/date.md b/docs/src/filters/date.md new file mode 100644 index 0000000..d9b3f21 --- /dev/null +++ b/docs/src/filters/date.md @@ -0,0 +1,19 @@ +## Date Filters + +## DateRangeFilter + +Filter dates. It allows complex filter like: + + + +| value | resulting filter | +|----------------------------------|------------------------------------| +| 2000-01-01 | equals 2000-01-01 | +| =2000-01-01 | equals 2000-01-01 | +| > 2000-01-01 | greater than 2000-01-03 | +| >= 2000-01-01 | greater or equal than 2000-01-03 | +| < 2000-01-01 | lower than 2000-01-03 | +| <= 2000-01-01 | lower or equal than 2000-01-03 | +| 2000-01-02..2000-12-02 | between 2000-01-02 and 2000-12-02 | +| <> 2000-12-02 | not equal to 2000-01-02 | +| 2000-01-01,2000-02-01,2000-03-01 | list of values | diff --git a/docs/filters/dj.rst b/docs/src/filters/dj.md similarity index 67% rename from docs/filters/dj.rst rename to docs/src/filters/dj.md index 895ad8b..b2b1607 100644 --- a/docs/filters/dj.rst +++ b/docs/src/filters/dj.md @@ -1,22 +1,14 @@ -.. include:: ../globals.rst +# Django Filters -.. _filters_dj: +## DjangoLookupFilter - -DjangoLookupFilter -================== - -.. image:: ../images/dj.png - :width: 200 +![dj](../static/images/dj.png){width=200} This filter allow you to use any lookups allowed in Django queries, can work on direct fields as well as on foreign keys. -Usage ------ - -python:: +## Usage class MyModelAdmin(AdminFiltersMixin, models.ModelAdmin): @@ -25,33 +17,32 @@ python:: ... ) -Options -~~~~~~~ - -.. attribute:: DjangoLookupFilter.can_negate +## Options +- DjangoLookupFilter.can_negate + Control ability to work as `exclude` filter. Set to `False` hides the Exclude checkbox -.. attribute:: DjangoLookupFilter.placeholder +- DjangoLookupFilter.placeholder Placeholder value for the Key input text. (Default. "field value") -.. attribute:: DjangoLookupFilter.field_placeholder +- DjangoLookupFilter.field_placeholder Placeholder value for Value input text. (Default. "field lookup. Es. name__startswith") -.. attribute:: DjangoLookupFilter.template +- DjangoLookupFilter.template Template name used to render the filter. (Default. "adminfilters/dj.html") -.. attribute:: DjangoLookupFilter.title +- DjangoLookupFilter.title Filter title. (Default. "Django Lookup") -Configuration -~~~~~~~~~~~~~ +## Configuration + -The filter can be configured either using subclassing or `.factory()` method:: +The filter can be configured either using subclassing or `.factory()` method: class MyModelAdmin(models.ModelAdmin): list_filter = ( diff --git a/docs/filters/json.rst b/docs/src/filters/json.md similarity index 66% rename from docs/filters/json.rst rename to docs/src/filters/json.md index 4298edc..77df5d4 100644 --- a/docs/filters/json.rst +++ b/docs/src/filters/json.md @@ -1,20 +1,13 @@ -.. include:: ../globals.rst +# JsonField Filter -.. _filters_json: +![autocomplete](../static/images/json.gif){width=200} -JsonFilter -========== +Specialized filter for [JSONField](https://docs.djangoproject.com/it/4.0/ref/models/fields/#django.db.models.JSONField). +I allows to filter nested json structure and handle different data types. -.. image:: ../images/json.gif - :width: 200 - -Specialized filter for JSONField_. I allows to filter nested json structure and handle different data types. - - -Usage ------ +## Usage python:: @@ -27,14 +20,13 @@ python:: ... ) -Options -~~~~~~~ +### Options -.. attribute:: JsonFilter.can_negate +- JsonFilter.can_negate Control ability to work as `exclude` filter. Set to `False` hides the Exclude checkbox -.. attribute:: JsonFilter.options +- JsonFilter.options It enable/disable option policy selection. Defines how the filter should treat records with missing key records. @@ -43,24 +35,23 @@ Options - `add missing`: includes records that do not have the selected key -.. attribute:: JsonFilter.placeholder +- JsonFilter.placeholder Placeholder value for the Key input text. (Default. "JSON key") -.. attribute:: JsonFilter.key_placeholder +- JsonFilter.key_placeholder Placeholder value for Value input text. (Default. "JSON value") -.. attribute:: JsonFilter.template +- JsonFilter.template Template name used to render the filter. (Default. "adminfilters/json.html") -.. attribute:: JsonFilter.title +- JsonFilter.title Filter title. (Default. "") -Configuration -~~~~~~~~~~~~~ +###Configuration The filter can be configured either using subclassing or `.factory()` method:: @@ -70,6 +61,3 @@ The filter can be configured either using subclassing or `.factory()` method:: title=_("FLAGS"))), ... ) - - -.. _JSONField: https://docs.djangoproject.com/it/4.0/ref/models/fields/#django.db.models.JSONField diff --git a/docs/src/filters/multiselect.md b/docs/src/filters/multiselect.md new file mode 100644 index 0000000..88e6206 --- /dev/null +++ b/docs/src/filters/multiselect.md @@ -0,0 +1,25 @@ +# Many To Many + +## IntersectionFieldListFilter + +![intersection](../static/images/intersection.gif){width=400} + +A FieldListFilter which allows multiple selection of filters for many-to-many type fields. A list of objects will be +returned whose m2m contains all the selected filters. + +### Usage + + class DemoModelAdmin_UnionFieldListFilter(DebugMixin, ModelAdmin): + list_filter = (("bands", IntersectionFieldListFilter),) + +## UnionFieldListFilter + +![union](../static/images/union.gif){width=400} + +A FieldListFilter which allows multiple selection of filters for many-to-many type fields, or any type with choices. A +list of objects will be returned whose m2m or value set contains one of the selected filters. + +### Usage + + class DemoModelAdmin_UnionFieldListFilter(DebugMixin, ModelAdmin): + list_filter = (("bands", UnionFieldListFilter),) diff --git a/docs/filters/multivalue.rst b/docs/src/filters/multivalue.md similarity index 72% rename from docs/filters/multivalue.rst rename to docs/src/filters/multivalue.md index f73c255..09df3d1 100644 --- a/docs/filters/multivalue.rst +++ b/docs/src/filters/multivalue.md @@ -1,20 +1,14 @@ -.. include:: ../globals.rst - -.. _filters_multivalue: - +# Multiple Choices MultiValueFilter ================ -.. image:: ../images/multivalue.gif - :width: 200 +![multivalue](../static/images/multivalue.gif){width=400} Targets can be typed as comma separated list of values. -Usage ------ -:: +## Usage class MyModelAdmin(models.ModelAdmin): list_filter = ( @@ -23,27 +17,27 @@ Usage ) -Options -~~~~~~~ +## Options -.. attribute:: MultiValueFilter.can_negate + +- MultiValueFilter.can_negate Control ability to work as `exclude` filter. Set to `False` hides the Exclude checkbox -.. attribute:: MultiValueFilter.placeholder +- MultiValueFilter.placeholder Placeholder value for the Key input text. (Default. "JSON key") -.. attribute:: MultiValueFilter.template +- MultiValueFilter.template Template name used to render the filter. (Default. "adminfilters/value.html") -.. attribute:: MultiValueFilter.title +- MultiValueFilter.title Filter title. (Default. "") -Configuration -~~~~~~~~~~~~~ +## Configuration + The filter can be configured either using subclassing or ``.factory()`` method:: diff --git a/docs/src/filters/numbers.md b/docs/src/filters/numbers.md new file mode 100644 index 0000000..adf1583 --- /dev/null +++ b/docs/src/filters/numbers.md @@ -0,0 +1,20 @@ +# Numbers + +## NumberFilter + +Filter numbers. It allows complex filter like: + + + + +| value | resulting filter | +|-------------|----------------------------------| +| 100 | equals 100 | +| =100 | equals 100 | +| > 100 | greater than 2000-01-03 | +| >= 100 | greater or equal than 2000-01-03 | +| < 100 | lower than 2000-01-03 | +| <= 100 | lower or equal than 2000-01-03 | +| 100..200 | between 100 and 200 | +| <> 200 | not equal 200 | +| 100,200,300 | list of values | diff --git a/docs/filters/querystring.rst b/docs/src/filters/querystring.md similarity index 84% rename from docs/filters/querystring.rst rename to docs/src/filters/querystring.md index c804835..1c1cb0d 100644 --- a/docs/filters/querystring.rst +++ b/docs/src/filters/querystring.md @@ -1,22 +1,16 @@ -.. include:: ../globals.rst +[//]: # (# QueryString) -.. _filters_qs: +## QueryStringFilter - -QueryStringFilter -================= - -.. image:: ../images/querystring.gif - :width: 200 +[//]: # (![querystring](../static/images/querystring.gif){width=400}) Allows complex filtering criteria, each line represent a ``field/value`` filter entry. Each line can be negated using ``!`` prefix -Usage ------ -:: +### Usage + class MyModelAdmin(AdminFiltersMixin, models.ModelAdmin): list_filter = ( @@ -24,8 +18,8 @@ Usage ... ) -Options -~~~~~~~ +### Options + .. attribute:: QueryStringFilter.can_negate @@ -43,8 +37,8 @@ Options Filter title. (Default. "QueryString") -Configuration -~~~~~~~~~~~~~ +### Configuration + The filter can be configured either using subclassing or `.factory()` method:: @@ -55,17 +49,15 @@ The filter can be configured either using subclassing or `.factory()` method:: ) -Usage Examples -~~~~~~~~~~~~~~ +### Usage Examples + -:: # Model.objects.filter(year=1972, name__istartswith="Annie") year=1972 name__istartswith=Annie -:: # Model.objects.filter(date__gt="2021-10-1").exclude(date__month=12) diff --git a/docs/src/filters/radio.md b/docs/src/filters/radio.md new file mode 100644 index 0000000..4be1aec --- /dev/null +++ b/docs/src/filters/radio.md @@ -0,0 +1,18 @@ +# Radio + +![boolean_radio](../static/images/boolean_radio.gif){width=300} + + + +.. image:: ../images/boolean_radio.gif + :width: 900 + + + +## AllValuesRadioFilter + +## BooleanRadioFilter + +## ChoicesFieldRadioFilter + +## RelatedFieldRadioFilter diff --git a/docs/filters/value.rst b/docs/src/filters/value.md similarity index 77% rename from docs/filters/value.rst rename to docs/src/filters/value.md index 4da9422..6d2b7b1 100644 --- a/docs/filters/value.rst +++ b/docs/src/filters/value.md @@ -1,22 +1,15 @@ -.. include:: ../globals.rst +# Exact Match -.. _filters_value: +## ValueFilter - -ValueFilter -=========== - -.. image:: ../images/value.png - :width: 200 +![value](../static/images/value.png){width=300} Filter that allow you to type the desired value, can works either with direct fields as withn foreign keys. By default it uses `exact` lookup -Usage ------ -:: +### Usage class MyModelAdmin(AdminFiltersMixin, models.ModelAdmin): list_filter = ( @@ -27,27 +20,26 @@ Usage ) -Options -~~~~~~~ +### Options -.. attribute:: ValueFilter.can_negate +- ValueFilter.can_negate Control ability to work as `exclude` filter. Set to `False` hides the Exclude checkbox -.. attribute:: ValueFilter.placeholder +- ValueFilter.placeholder Placeholder value for the Key input text. (Default. "JSON key") -.. attribute:: ValueFilter.template +- ValueFilter.template Template name used to render the filter. (Default. "adminfilters/value.html") -.. attribute:: ValueFilter.title +- ValueFilter.title Filter title. (Default. "") -Configuration -~~~~~~~~~~~~~ +### Configuration + The filter can be configured either using subclassing or `.factory()` method:: diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..d39a6ee --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,19 @@ +# django-adminfilters + +Collection of useful filters to use with django admin site with low impact and no dependencies with external libraries. + + +
+ +
+ +## Links + + + - Project home page: https://github.com/saxix/django-adminfilters + - Issue tracker: https://github.com/saxix/django-adminfilters/issues?sort + - Pypi: https://badge.fury.io/py/django-adminfilters + - Download: http://pypi.python.org/pypi/django-adminfilters/ + - Documentation: http://readthedocs.org/docs/django-adminfilters/en/latest/ diff --git a/docs/src/install.md b/docs/src/install.md new file mode 100644 index 0000000..42722b6 --- /dev/null +++ b/docs/src/install.md @@ -0,0 +1,12 @@ +# Install + + + pip install django-adminfilters + + +After installation add it to ``INSTALLED_APPS`` + + INSTALLED_APPS = ( + ... + 'adminfilters', + ) diff --git a/docs/adminfilters.mp4 b/docs/src/static/adminfilters.mp4 similarity index 100% rename from docs/adminfilters.mp4 rename to docs/src/static/adminfilters.mp4 diff --git a/docs/src/static/adminfilters.png b/docs/src/static/adminfilters.png new file mode 100644 index 0000000..3009267 Binary files /dev/null and b/docs/src/static/adminfilters.png differ diff --git a/docs/images/ChoicesFieldComboFilter.gif b/docs/src/static/images/ChoicesFieldComboFilter.gif similarity index 100% rename from docs/images/ChoicesFieldComboFilter.gif rename to docs/src/static/images/ChoicesFieldComboFilter.gif diff --git a/docs/src/static/images/ChoicesFieldComboFilter.mov b/docs/src/static/images/ChoicesFieldComboFilter.mov new file mode 100644 index 0000000..4deb305 Binary files /dev/null and b/docs/src/static/images/ChoicesFieldComboFilter.mov differ diff --git a/docs/images/IntersectionFieldListFilter.gif b/docs/src/static/images/IntersectionFieldListFilter.gif similarity index 100% rename from docs/images/IntersectionFieldListFilter.gif rename to docs/src/static/images/IntersectionFieldListFilter.gif diff --git a/docs/src/static/images/IntersectionFieldListFilter.mov b/docs/src/static/images/IntersectionFieldListFilter.mov new file mode 100644 index 0000000..12985db Binary files /dev/null and b/docs/src/static/images/IntersectionFieldListFilter.mov differ diff --git a/docs/images/UnionFieldListFilter.gif b/docs/src/static/images/UnionFieldListFilter.gif similarity index 100% rename from docs/images/UnionFieldListFilter.gif rename to docs/src/static/images/UnionFieldListFilter.gif diff --git a/docs/src/static/images/UnionFieldListFilter.mov b/docs/src/static/images/UnionFieldListFilter.mov new file mode 100644 index 0000000..65bbed9 Binary files /dev/null and b/docs/src/static/images/UnionFieldListFilter.mov differ diff --git a/docs/images/autocomplete.gif b/docs/src/static/images/autocomplete.gif similarity index 100% rename from docs/images/autocomplete.gif rename to docs/src/static/images/autocomplete.gif diff --git a/docs/src/static/images/autocomplete.mov b/docs/src/static/images/autocomplete.mov new file mode 100644 index 0000000..2a3645b Binary files /dev/null and b/docs/src/static/images/autocomplete.mov differ diff --git a/docs/images/boolean_radio.gif b/docs/src/static/images/boolean_radio.gif similarity index 100% rename from docs/images/boolean_radio.gif rename to docs/src/static/images/boolean_radio.gif diff --git a/docs/src/static/images/boolean_radio.mov b/docs/src/static/images/boolean_radio.mov new file mode 100644 index 0000000..4d384ae Binary files /dev/null and b/docs/src/static/images/boolean_radio.mov differ diff --git a/docs/images/choices_field_combo.gif b/docs/src/static/images/choices_field_combo.gif similarity index 100% rename from docs/images/choices_field_combo.gif rename to docs/src/static/images/choices_field_combo.gif diff --git a/docs/images/depot.gif b/docs/src/static/images/depot.gif similarity index 100% rename from docs/images/depot.gif rename to docs/src/static/images/depot.gif diff --git a/docs/src/static/images/depot.mov b/docs/src/static/images/depot.mov new file mode 100644 index 0000000..3009d91 Binary files /dev/null and b/docs/src/static/images/depot.mov differ diff --git a/docs/images/depot.png b/docs/src/static/images/depot.png similarity index 100% rename from docs/images/depot.png rename to docs/src/static/images/depot.png diff --git a/docs/images/dj.png b/docs/src/static/images/dj.png similarity index 100% rename from docs/images/dj.png rename to docs/src/static/images/dj.png diff --git a/docs/images/intersection.gif b/docs/src/static/images/intersection.gif similarity index 100% rename from docs/images/intersection.gif rename to docs/src/static/images/intersection.gif diff --git a/docs/src/static/images/intersection.mov b/docs/src/static/images/intersection.mov new file mode 100644 index 0000000..7300d7b Binary files /dev/null and b/docs/src/static/images/intersection.mov differ diff --git a/docs/images/json.gif b/docs/src/static/images/json.gif similarity index 100% rename from docs/images/json.gif rename to docs/src/static/images/json.gif diff --git a/docs/src/static/images/json.mov b/docs/src/static/images/json.mov new file mode 100644 index 0000000..0d58128 Binary files /dev/null and b/docs/src/static/images/json.mov differ diff --git a/docs/images/multivalue.gif b/docs/src/static/images/multivalue.gif similarity index 100% rename from docs/images/multivalue.gif rename to docs/src/static/images/multivalue.gif diff --git a/docs/src/static/images/multivalue.mov b/docs/src/static/images/multivalue.mov new file mode 100644 index 0000000..3e75e92 Binary files /dev/null and b/docs/src/static/images/multivalue.mov differ diff --git a/docs/images/querystring.gif b/docs/src/static/images/querystring.gif similarity index 100% rename from docs/images/querystring.gif rename to docs/src/static/images/querystring.gif diff --git a/docs/src/static/images/querystring.mov b/docs/src/static/images/querystring.mov new file mode 100644 index 0000000..f97351f Binary files /dev/null and b/docs/src/static/images/querystring.mov differ diff --git a/docs/images/union.gif b/docs/src/static/images/union.gif similarity index 100% rename from docs/images/union.gif rename to docs/src/static/images/union.gif diff --git a/docs/src/static/images/union.mov b/docs/src/static/images/union.mov new file mode 100644 index 0000000..6e1534b Binary files /dev/null and b/docs/src/static/images/union.mov differ diff --git a/docs/images/value.png b/docs/src/static/images/value.png similarity index 100% rename from docs/images/value.png rename to docs/src/static/images/value.png diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..99a59be --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,74 @@ +dev_addr: 127.0.0.1:9000 +docs_dir: docs/src +edit_uri: "/tree/develop/docs/source" +repo_url: https://github.com/saxix/django-adminfilters +site_description: "" +site_dir: ./~build/docs +site_name: Documentation +site_url: https://github.com/saxix/django-adminfilters +strict: false + + +markdown_extensions: + - admonition + - pymdownx.magiclink + - mdx_gh_links: + user: saxix + repo: django-adminfilters + - markdown_include.include: + base_path: . + - attr_list + - md_in_html + + +theme: + name: "material" + color_mode: auto +# highlightjs: true +# hljs_languages: +# - yaml +# - django + user_color_mode_toggle: true + features: + - content.action.edit + - content.code.annotate + - content.code.copy + - content.tooltips + - header.autohidex + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.progress + extra: + version: + provider: mike + alias: true + palette: + # Palette toggle for light mode + - scheme: default + primary: light blue + media: "(prefers-color-scheme: light)" + toggle: + icon: material/weather-sunny + name: Switch to dark mode + + # Palette toggle for dark mode + - scheme: slate + primary: light blue + media: "(prefers-color-scheme: dark)" + toggle: + icon: material/weather-night + name: Switch to light mode + +plugins: + - mkdocstrings: + default_handler: python + - awesome-pages + - search + - autolinks + + +watch: + - docs/ + - src/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b299342 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[project] +name = "django-adminfilters" +version = "2.5.0" +description = "Django mixin to easily add buttons to any ModelAdmin" +readme = "README.md" +requires-python = ">=3.11" +classifiers=[ + 'Environment :: Web Environment', + 'Operating System :: OS Independent', + 'Framework :: Django', + 'Framework :: Django :: 4.2', + 'Framework :: Django :: 5.0', + 'Framework :: Django :: 5.1', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Intended Audience :: Developers' + ] + +dependencies = [ +] + +[project.urls] +Homepage = "https://github.com/saxix/django-adminfilterst" +Documentation = "https://github.com/saxix/django-adminfilters" + +[project.optional-dependencies] +docs = [ + "mkdocs>=1.6.1", + "markdown-include", + "mkdocs-material>=9.5.36", + "mkdocs-awesome-pages-plugin>=2.9.3", + "mkdocstrings-python", + "mdx-gh-links>=0.4", +] + +[tool.uv] +dev-dependencies = [ + "bump2version>=1.0.1", + "check-manifest>=0.50", + "django-environ>=0.11.2", + "factory-boy>=3.3.1", + "flake8>=7.1.1", + "isort>=5.13.2", + "mkdocs-autolinks-plugin>=0.7.1", + "pdbpp>=0.10.3", + "prettytable==3.9.0", + "psycopg2-binary>=2.9.9", + "pytest-cov>=5.0.0", + "pytest-django>=4.5.2", + "pytest-echo>=1.7.3", + "pytest-selenium<4", + "pytest<7", + "selenium==4.9.1", + "tox>=4.21.2", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..6577fd6 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,27 @@ +[pytest] +norecursedirs = data .tox _plugin_template .idea node_modules ~* +log_format = %(asctime)s %(levelname)s %(message)s +log_level = CRITICAL +django_find_project = false +log_cli = False +log_date_format = %Y-%m-%d %H:%M:%S +junit_family=xunit1 +pythonpath=src +testpaths=tests +tmp_path_retention_policy=all +tmp_path_retention_count=0 +addopts = + -rs + --tb=short + --capture=sys + --cov-config=tests/.coveragerc + --cov-report html + --cov-report xml:coverage.xml + + +markers = + admin + eager + selenium + +python_files=test_*.py diff --git a/setup.py b/setup.py deleted file mode 100755 index b426b23..0000000 --- a/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python -import ast -import codecs -import os -import re - -from setuptools import find_packages, setup - -ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__))) -init = os.path.join(ROOT, 'src', 'adminfilters', '__init__.py') - -_version_re = re.compile(r'__version__\s+=\s+(.*)') -_name_re = re.compile(r'NAME\s+=\s+(.*)') - -with open(init, 'rb') as f: - content = f.read().decode('utf-8') - version = str(ast.literal_eval(_version_re.search(content).group(1))) - name = str(ast.literal_eval(_name_re.search(content).group(1))) - - -def read(*parts): - here = os.path.abspath(os.path.dirname(__file__)) - return codecs.open(os.path.join(here, 'src', 'requirements', *parts), 'r').read() - - -install_requires = read('install.pip') -tests_requires = read('testing.pip') -dev_requires = tests_requires + read('develop.pip') - -setup( - name=name, - version=version, - url='https://github.com/saxix/django-adminfilters', - download_url='https://github.com/saxix/django-adminfilters', - author='sax', - author_email='s.apostolico@gmail.com', - description='Extra filters for django admin site', - license='MIT', - package_dir={'': 'src'}, - packages=find_packages('src'), - include_package_data=True, - extras_require={'test': tests_requires, 'dev': dev_requires}, - platforms=['any'], - classifiers=[ - 'Environment :: Web Environment', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.2', - 'Framework :: Django :: 4.0', - 'Framework :: Django :: 5.0', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Intended Audience :: Developers', - ], - long_description=codecs.open('README.md', 'r').read(), - long_description_content_type='text/markdown', -) diff --git a/src/adminfilters/__init__.py b/src/adminfilters/__init__.py index 83467d4..2c148cd 100644 --- a/src/adminfilters/__init__.py +++ b/src/adminfilters/__init__.py @@ -1,3 +1,3 @@ NAME = "django-adminfilters" -VERSION = __version__ = "2.4.3" +VERSION = __version__ = "2.5.0" __author__ = "sax" diff --git a/src/adminfilters/checkbox.py b/src/adminfilters/checkbox.py index 5379422..86d412e 100644 --- a/src/adminfilters/checkbox.py +++ b/src/adminfilters/checkbox.py @@ -69,9 +69,9 @@ def choices(self, cl): [self.lookup_kwarg_isnull], ), "display": val, - "uncheck_to_remove": "{}={}".format(self.lookup_kwarg, pk_val) - if pk_val - else "", + "uncheck_to_remove": ( + "{}={}".format(self.lookup_kwarg, pk_val) if pk_val else "" + ), } if ( isinstance(self.field, ForeignObjectRel) diff --git a/src/adminfilters/compat.py b/src/adminfilters/compat.py index a5b600e..377a12e 100644 --- a/src/adminfilters/compat.py +++ b/src/adminfilters/compat.py @@ -1,3 +1,3 @@ from django import __version__ as django_version -DJANGO_MAJOR = int(django_version.split('.')[0]) +DJANGO_MAJOR = int(django_version.split(".")[0]) diff --git a/src/adminfilters/dj.py b/src/adminfilters/dj.py index 0025718..6a7c98d 100644 --- a/src/adminfilters/dj.py +++ b/src/adminfilters/dj.py @@ -27,7 +27,9 @@ def __init__(self, request, params, model, model_admin): self._params = params self.lookup_field_val = self.get_parameters(self.lookup_kwarg_key, pop=True) self.lookup_value_val = self.get_parameters(self.lookup_kwarg_value, pop=True) - self.lookup_negated_val = self.get_parameters(self.lookup_kwarg_negated, "false", pop=True) + self.lookup_negated_val = self.get_parameters( + self.lookup_kwarg_negated, "false", pop=True + ) self.error_message = None self.exception = None self.filters = None diff --git a/src/adminfilters/mixin.py b/src/adminfilters/mixin.py index 4697e7e..b3763e1 100644 --- a/src/adminfilters/mixin.py +++ b/src/adminfilters/mixin.py @@ -2,6 +2,7 @@ from django.contrib.admin import FieldListFilter, ListFilter from django.contrib.admin.options import ModelAdmin from django.core import checks +from django.core.exceptions import FieldDoesNotExist from adminfilters.compat import DJANGO_MAJOR @@ -26,7 +27,9 @@ def __init__(self, *args, **kwargs) -> None: f"{self.model_admin.__class__.__name__} must inherit from AdminFiltersMixin" ) - def get_parameters(self, param_name, default="", multi=False, pop=False, separator=","): + def get_parameters( + self, param_name, default="", multi=False, pop=False, separator="," + ): if pop: val = self._params.pop(param_name, default) else: @@ -93,19 +96,44 @@ def _check_linked_fields_modeladmin(self): parts = entry[1].parent.split("__") m = self.model for part in parts: - m = m._meta.get_field(part).remote_field.model - ma: ModelAdmin = self.admin_site._registry[m] - if ma not in seen and not isinstance( - ma, AdminAutoCompleteSearchMixin - ): + try: + m = m._meta.get_field(part).remote_field.model + except FieldDoesNotExist as e: errs.append( checks.Error( - f"{ma}` must inherits from AdminAutoCompleteSearchMixin", - obj=ma.__class__, - id="admin.E041", + f"{m}` {e}", + obj=self, + id="adminfilters.E001", ) ) - seen.append(ma) + else: + try: + ma: ModelAdmin = self.admin_site._registry[m] + except KeyError: + for proxy in m.__subclasses__(): + if proxy in self.admin_site._registry: + ma = self.admin_site._registry[proxy] + break + else: + errs.append( + checks.Error( + f"{m}` is not registered in {self.admin_site}", + obj=self, + id="adminfilters.E002", + ) + ) + else: + if ma not in seen and not isinstance( + ma, AdminAutoCompleteSearchMixin + ): + errs.append( + checks.Error( + f"{ma}` must inherits from AdminAutoCompleteSearchMixin", + obj=ma.__class__, + id="adminfilters.E003", + ) + ) + seen.append(ma) return errs diff --git a/src/adminfilters/querystring.py b/src/adminfilters/querystring.py index 144d179..f44aa0d 100644 --- a/src/adminfilters/querystring.py +++ b/src/adminfilters/querystring.py @@ -28,7 +28,9 @@ def __init__(self, request, params, model, model_admin): self.parameter_name_negated = "%s__negate" % self.parameter_name self._params = params self.lookup_field_val = self.get_parameters(self.parameter_name, pop=True) - self.lookup_negated_val = self.get_parameters(self.parameter_name_negated, "false", pop=True) + self.lookup_negated_val = self.get_parameters( + self.parameter_name_negated, "false", pop=True + ) self.query_string = None self.error_message = None self.exception = None diff --git a/src/adminfilters/utils.py b/src/adminfilters/utils.py index 4fce2aa..ae85e5a 100644 --- a/src/adminfilters/utils.py +++ b/src/adminfilters/utils.py @@ -61,9 +61,11 @@ def get_all_field_names(model): return list( set( chain.from_iterable( - (field.name, field.attname) - if hasattr(field, "attname") - else (field.name,) + ( + (field.name, field.attname) + if hasattr(field, "attname") + else (field.name,) + ) for field in model._meta.get_fields() if not (field.many_to_one and field.related_model is None) ) diff --git a/tox.ini b/tox.ini index 59f53c0..d147e0f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,72 +1,79 @@ [tox] -envlist = - d32-py{39,310,311} - d{42,50}-py{310,311,312} +envlist = d{42,51}-py{311,312} skip_missing_interpreters = true - -[pytest] -;python_paths=./tests/demoapp -django_find_project = false -log_format = %(asctime)s %(levelname)s %(message)s -log_level = CRITICAL -norecursedirs = demo .tox -addopts = - --tb=short - --reuse-db - --capture=no - --echo-version django - -markers = - selenium: Run selenium functional tests -filterwarnings = - ignore::DeprecationWarning - -[testenv:lint] -envdir={toxworkdir}/d32-py39/ -skip_install = true -deps= -; black -; flake8 -; isort - -rsrc/requirements/testing.pip - -rsrc/requirements/develop.pip - pre-commit - -commands = -; black --check ./src ./tests - pre-commit run --all-files -; flake8 src tests -; isort src tests --check --settings .isort.cfg +;skipsdist = true [testenv] +skip_install = true passenv = - PYTHONPATH + PYTHONDONTWRITEBYTECODE DATABASE_URL + DOCKER_DEFAULT_PLATFORM -setenv = -whitelist_externals = mkdir deps = - -rsrc/requirements/testing.pip - d32: django==3.2.* - d42: django==4.2.* - d50: django==5.0.* + uv + pip + +;allowlist_externals = +; uv +; sh + +changedir={toxinidir} +setenv = + d42: DJANGO = django==4.2.* + d51: DJANGO = django==5.1.* + d42: LOCK = "uv4.lock" + d51: LOCK = "uv5.lock" + +extras = + dev commands = - {posargs:py.test tests/functional --create-db --selenium --cov-report=xml --cov-report=term --junitxml=pytest.xml \ - --cov-config=tests/.coveragerc --cov adminfilters} + uv venv {work_dir}/.venv + uv pip list + uv export -q --no-hashes -o {work_dir}/requirements.txt + pip install -r {work_dir}/requirements.txt + pip install '{env:DJANGO}' + pytest tests + + +[testenv:lint] +envdir={toxworkdir}/d42-py312/ +skip_install = true +commands = + uv run flake8 src tests + uv run isort -c src tests + + +[testenv:docs] +extras = + docs +commands = + uv run mkdocs build + [testenv:package] +skip_install = true +allowlist_externals = + grep + tr + cut + sh + echo + deps= build twine + pip + setenv = TWINE_USERNAME = {env:TWINE_TEST_USERNAME:__token__} TWINE_PASSWORD = {env:TWINE_TEST_PASSWORD} commands = - python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" - python -m build - python -m twine check dist/* - python -m twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/* + python -c "import shutil; shutil.rmtree('{toxinidir}/dist', ignore_errors=True)" + python -m build --outdir {toxinidir}/dist + pip install django-admin-extra-buttons --use-pep517 --no-deps --no-cache-dir --find-links file://{toxinidir}/dist/ + python scripts/check_version.py