From d9eecedc5f43c5f9a9092ba111a6f463e9a8f7ca Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Thu, 6 Jun 2024 23:36:29 -0400 Subject: [PATCH 01/10] cleanup & structural changes --- .editorconfig | 28 ++ .github/dependabot.yml | 11 + .github/pull_request_template.md | 11 + .github/workflows/validate.yml | 39 ++ .gitignore | 110 +++--- .pre-commit-config.yaml | 38 ++ .travis.yml | 39 -- MANIFEST.in | 5 - Makefile | 5 + README.md | 21 + README.rst | 634 ------------------------------- codecov.yml | 2 - manage.py | 23 -- pyproject.toml | 66 ++++ runtests.py | 15 + setup.cfg | 75 ---- setup.py | 4 - tox.ini | 71 ---- 18 files changed, 287 insertions(+), 910 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/validate.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 .travis.yml delete mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 codecov.yml delete mode 100644 manage.py create mode 100644 pyproject.toml create mode 100644 runtests.py delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tox.ini diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1714ae0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,28 @@ +# Defines the coding style for different editors and IDEs. +# http://editorconfig.org + +# top-most EditorConfig file +root = true + +# Rules for source code. +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + +# Rules for Python code. +[*.py] +indent_size = 4 + +# Rules for markdown documents. +[*.md] +indent_size = 4 +trim_trailing_whitespace = false + +# Rules for makefile +[Makefile] +indent_style = tab +indent_size = 4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9d866e3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d9296f6 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +### Summary +- Write a quick summary of what this PR is for + + +##### Related Links +- Paste link to ticket or any other related sites here + +##### Ready for QA Checklist +- [ ] Code Review +- [ ] Dev QA +- [ ] Rebase and Squash diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..c3910ba --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,39 @@ +name: Validate + +on: + push: + branches: + - develop + - master + - main + - 'release/**' + pull_request: + branches: + - '*' + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools poetry + poetry install + - name: Linting + run: | + poetry run isort . + poetry run black . + poetry run flake8 . --extend-ignore=D,E501,W601 --extend-exclude=docs/ --statistics --count + - name: Security + run: poetry run bandit -c pyproject.toml -r . + - name: Testing + run: poetry run python ./runtests.py diff --git a/.gitignore b/.gitignore index e72411f..54f234e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,62 +1,58 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg +*.py[co] +*.swp +*.bak + +# docs +_build + +# Packages *.egg -MANIFEST +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt # Unit test / coverage reports -htmlcov/ -.tox/ .coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations +.tox + +.idea + +.DS_Store + +#Translations *.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Sphinx documentation -docs/_build/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ + +#Mr Developer +.mr.developer.cfg + +# Configuration +sdelint.cnf + +#generated data +usecases/output.csv + +# Test files +info.log +htmlcov/ + +#ides +.idea +.vscode + +#custom cert bundle +my_root_certs.crt + +# symbolic links +.flake8 + +conf/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5a073f0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +repos: +- repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.1.13 + hooks: + - id: forbid-crlf + - id: remove-crlf + - id: forbid-tabs + exclude_types: [csv] + - id: remove-tabs + exclude_types: [csv] + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-yaml + args: [--unsafe] + +- repo: https://github.com/pre-commit/mirrors-isort + rev: v5.10.1 + hooks: + - id: isort + +- repo: https://github.com/ambv/black + rev: 22.3.0 + hooks: + - id: black + language_version: python3.12 + +- repo: https://github.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: [flake8-typing-imports==1.10.0] + exclude: ^tests + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cb23ec1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -language: python -dist: bionic - -python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" - -install: - - travis_retry pip install -U six setuptools pip wheel - - travis_retry pip install -U tox tox-travis coverage -script: - - tox -after_success: - - coverage report - - bash <(curl -s https://codecov.io/bash) - -matrix: - include: - - python: "3.8" - env: TOXENV="performance" - script: tox -- -v 2 - - python: "3.8" - env: TOXENV="warnings" - - python: "3.8" - env: TOXENV="isort,lint" - - - python: "3.8" - env: TOXENV="dist" - script: - - python setup.py bdist_wheel - - rm -r djangorestframework_filters.egg-info - - tox --installpkg ./dist/djangorestframework_filters-*.whl - - tox # test sdist - - allow_failures: - - env: TOXENV="warnings" - fast_finish: true diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 8a26345..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include LICENSE -include README.rst -recursive-include rest_framework_filters/templates *.html -global-exclude __pycache__ -global-exclude *.py[co] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1515db5 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +lint: + poetry run flake8 . --extend-ignore=D,E501,W601 --extend-exclude=docs/ --statistics --count + +test: + poetry run python ./runtests.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..7241ddf --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# django-rest-framework-filters + +django-rest-framework-filters implements a very simple database-based key-value store for Django. + +This package is a fork and only maintained on an as needed basis since the upstream package is no longer maintained at - https://github.com/vikingco/django-rest-framework-filters. + +## Install + +```bash +pip install git+https://github.com/sdelements/django-rest-framework-filters@master +``` + +## Usage + +The interface is straightforward:: + +```python + from rest-framework-filters import get_value_for_key, set_key_value + set_key_value('foo', 'bar') + get_value_for_key('foo') +``` diff --git a/README.rst b/README.rst deleted file mode 100644 index e88bbdf..0000000 --- a/README.rst +++ /dev/null @@ -1,634 +0,0 @@ -Django Rest Framework Filters -============================= - -.. image:: https://travis-ci.org/philipn/django-rest-framework-filters.svg?branch=master - :target: https://travis-ci.org/philipn/django-rest-framework-filters - -.. image:: https://codecov.io/gh/philipn/django-rest-framework-filters/branch/master/graph/badge.svg - :target: https://codecov.io/gh/philipn/django-rest-framework-filters - -.. image:: https://img.shields.io/pypi/v/djangorestframework-filters.svg - :target: https://pypi.python.org/pypi/djangorestframework-filters - -.. image:: https://img.shields.io/pypi/pyversions/djangorestframework-filters.svg - :target: https://pypi.org/project/djangorestframework-filters/ - -.. image:: https://img.shields.io/pypi/l/tox-factor.svg - :target: https://pypi.org/project/djangorestframework-filters/ - - -``django-rest-framework-filters`` is an extension to `Django REST framework`_ and `Django filter`_ -that makes it easy to filter across relationships. Historically, this extension also provided a -number of additional features and fixes, however the number of features has shrunk as they are -merged back into ``django-filter``. - -.. _`Django REST framework`: https://github.com/tomchristie/django-rest-framework -.. _`Django filter`: https://github.com/carltongibson/django-filter - -Using ``django-rest-framework-filters``, we can easily do stuff like:: - - /api/article?author__first_name__icontains=john - /api/article?is_published!=true - ----- - -**!** These docs pertain to the upcoming 1.0 release. Current docs can be found `here`_. - -.. _`here`: https://github.com/philipn/django-rest-framework-filters/blob/v0.10.2/README.rst - ----- - -**!** The 1.0 pre-release is compatible with django-filter 2.x and can be installed with -``pip install --pre``. - ----- - -.. contents:: - **Table of Contents** - :local: - :depth: 2 - :backlinks: none - -Features --------- - -* Easy filtering across relationships. -* Support for method filtering across relationships. -* Automatic filter negation with a simple ``param!=value`` syntax. -* Backend for complex operations on multiple filtered querysets. eg, ``q1 | q2``. - - -Requirements ------------- - -* **Python**: 3.5, 3.6, 3.7, 3.8 -* **Django**: 1.11, 2.0, 2.1, 2.2, 3.0, 3.1 -* **DRF**: 3.11 -* **django-filter**: 2.1, 2.2 (Django 2.0+) - - -Installation ------------- - -Install with pip, or your preferred package manager: - -.. code-block:: bash - - $ pip install djangorestframework-filters - - -Add to your ``INSTALLED_APPS`` setting: - -.. code-block:: python - - INSTALLED_APPS = [ - 'rest_framework_filters', - ... - ] - - -``FilterSet`` usage -------------------- - -Upgrading from ``django-filter`` to ``django-rest-framework-filters`` is straightforward: - -* Import from ``rest_framework_filters`` instead of from ``django_filters`` -* Use the ``rest_framework_filters`` backend instead of the one provided by ``django_filter``. - -.. code-block:: python - - # django-filter - from django_filters.rest_framework import FilterSet, filters - - class ProductFilter(FilterSet): - manufacturer = filters.ModelChoiceFilter(queryset=Manufacturer.objects.all()) - ... - - - # django-rest-framework-filters - import rest_framework_filters as filters - - class ProductFilter(filters.FilterSet): - manufacturer = filters.ModelChoiceFilter(queryset=Manufacturer.objects.all()) - ... - - -To use the django-rest-framework-filters backend, add the following to your settings: - -.. code-block:: python - - REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ( - 'rest_framework_filters.backends.RestFrameworkFilterBackend', ... - ), - ... - - -Once configured, you can continue to use all of the filters found in ``django-filter``. - - -Filtering across relationships -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can easily traverse multiple relationships when filtering by using ``RelatedFilter``: - -.. code-block:: python - - from rest_framework import viewsets - import rest_framework_filters as filters - - - class ManagerFilter(filters.FilterSet): - class Meta: - model = Manager - fields = {'name': ['exact', 'in', 'startswith']} - - - class DepartmentFilter(filters.FilterSet): - manager = filters.RelatedFilter(ManagerFilter, field_name='manager', queryset=Manager.objects.all()) - - class Meta: - model = Department - fields = {'name': ['exact', 'in', 'startswith']} - - - class CompanyFilter(filters.FilterSet): - department = filters.RelatedFilter(DepartmentFilter, field_name='department', queryset=Department.objects.all()) - - class Meta: - model = Company - fields = {'name': ['exact', 'in', 'startswith']} - - - # company viewset - class CompanyView(viewsets.ModelViewSet): - filter_class = CompanyFilter - ... - -Example filter calls: - -.. code-block:: - - /api/companies?department__name=Accounting - /api/companies?department__manager__name__startswith=Bob - -``queryset`` callables -"""""""""""""""""""""" - -Since ``RelatedFilter`` is a subclass of ``ModelChoiceFilter``, the ``queryset`` argument supports callable behavior. -In the following example, the set of departments is restricted to those in the user's company. - -.. code-block:: python - - def departments(request): - company = request.user.company - return company.department_set.all() - - class EmployeeFilter(filters.FilterSet): - department = filters.RelatedFilter(filterset=DepartmentFilter, queryset=departments) - ... - -Recursive & Circular relationships -"""""""""""""""""""""""""""""""""" - -Recursive relations are also supported. Provide the module path as a string in place of the filterset class. - -.. code-block:: python - - class PersonFilter(filters.FilterSet): - name = filters.AllLookupsFilter(field_name='name') - best_friend = filters.RelatedFilter('people.views.PersonFilter', field_name='best_friend', queryset=Person.objects.all()) - - class Meta: - model = Person - - -This feature is also useful for circular relationships, where a related filterset may not yet be created. Note that -you can pass the related filterset by name if it's located in the same module as the parent filterset. - -.. code-block:: python - - class BlogFilter(filters.FilterSet): - post = filters.RelatedFilter('PostFilter', queryset=Post.objects.all()) - - class PostFilter(filters.FilterSet): - blog = filters.RelatedFilter('BlogFilter', queryset=Blog.objects.all()) - - -Supporting ``Filter.method`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``django_filters.MethodFilter`` has been deprecated and reimplemented as the ``method`` argument -to all filter classes. It incorporates some of the implementation details of the old -``rest_framework_filters.MethodFilter``, but requires less boilerplate and is simpler to write. - -* It is no longer necessary to perform empty/null value checking. -* You may use any filter class (``CharFilter``, ``BooleanFilter``, etc...) which will - validate input values for you. -* The argument signature has changed from ``(name, qs, value)`` to ``(qs, name, value)``. - -.. code-block:: python - - class PostFilter(filters.FilterSet): - # Note the use of BooleanFilter, the original model field's name, and the method argument. - is_published = filters.BooleanFilter(field_name='date_published', method='filter_is_published') - - class Meta: - model = Post - fields = ['title', 'content'] - - def filter_is_published(self, qs, name, value): - """ - `is_published` is based on the `date_published` model field. - If the publishing date is null, then the post is not published. - """ - # incoming value is normalized as a boolean by BooleanFilter - isnull = not value - lookup_expr = LOOKUP_SEP.join([name, 'isnull']) - - return qs.filter(**{lookup_expr: isnull}) - - class AuthorFilter(filters.FilterSet): - posts = filters.RelatedFilter('PostFilter', queryset=Post.objects.all()) - - class Meta: - model = Author - fields = ['name'] - -The above would enable the following filter calls: - -.. code-block:: - - /api/posts?is_published=true - /api/authors?posts__is_published=true - - -In the first API call, the filter method receives a queryset of posts. In the second, -it receives a queryset of users. The filter method in the example modifies the lookup -name to work across the relationship, allowing you to find published posts, or authors -who have published posts. - -Automatic Filter Negation/Exclusion -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -FilterSets support automatic exclusion using a simple ``param!=value`` syntax. This syntax -internally sets the ``exclude`` property on the filter. - -.. code-block:: - - /api/page?title!=The%20Park - -This syntax supports regular filtering combined with exclusion filtering. For example, the -following would search for all articles containing "Hello" in the title, while excluding -those containing "World". - -.. code-block:: - - /api/articles?title__contains=Hello&title__contains!=World - -Note that most filters only accept a single query parameter. In the above, ``title__contains`` -and ``title__contains!`` are interpreted as two separate query parameters. The following would -probably be invalid, although it depends on the specifics of the individual filter class: - -.. code-block:: - - /api/articles?title__contains=Hello&title__contains!=World&title_contains!=Friend - - -Allowing any lookup type on a field -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you need to enable several lookups for a field, django-filter provides the dict-syntax for -``Meta.fields``. - -.. code-block:: python - - class ProductFilter(filters.FilterSet): - class Meta: - model = Product - fields = { - 'price': ['exact', 'lt', 'gt', ...], - } - -``django-rest-framework-filters`` also allows you to enable all possible lookups for any field. -This can be achieved through the use of ``AllLookupsFilter`` or using the ``'__all__'`` value in -the ``Meta.fields`` dict-style syntax. Generated filters (``Meta.fields``, ``AllLookupsFilter``) -will never override your declared filters. - -Note that using all lookups comes with the same admonitions as enabling ``'__all__'`` fields in -django forms (`docs`_). Exposing all lookups may allow users to construct queries that -inadvertently leak data. Use this feature responsibly. - -.. _`docs`: https://docs.djangoproject.com/en/1.10/topics/forms/modelforms/#selecting-the-fields-to-use - -.. code-block:: python - - class ProductFilter(filters.FilterSet): - # Not overridden by `__all__` - price__gt = filters.NumberFilter(field_name='price', lookup_expr='gt', label='Minimum price') - - class Meta: - model = Product - fields = { - 'price': '__all__', - } - - # or - - class ProductFilter(filters.FilterSet): - price = filters.AllLookupsFilter() - - # Not overridden by `AllLookupsFilter` - price__gt = filters.NumberFilter(field_name='price', lookup_expr='gt', label='Minimum price') - - class Meta: - model = Product - -You cannot combine ``AllLookupsFilter`` with ``RelatedFilter`` as the filter names would clash. - -.. code-block:: python - - class ProductFilter(filters.FilterSet): - manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all()) - manufacturer = filters.AllLookupsFilter() - -To work around this, you have the following options: - -.. code-block:: python - - class ProductFilter(filters.FilterSet): - manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all()) - - class Meta: - model = Product - fields = { - 'manufacturer': '__all__', - } - - # or - - class ProductFilter(filters.FilterSet): - manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all(), lookups='__all__') # `lookups` also accepts a list - - class Meta: - model = Product - - -Can I mix and match ``django-filter`` and ``django-rest-framework-filters``? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Yes you can. ``django-rest-framework-filters`` is simply an extension of ``django-filter``. Note -that ``RelatedFilter`` and other ``django-rest-framework-filters`` features are designed to work -with ``rest_framework_filters.FilterSet`` and will not function on a ``django_filters.FilterSet``. -However, the target ``RelatedFilter.filterset`` may point to a ``FilterSet`` from either package, -and both ``FilterSet`` implementations are compatible with the other's DRF backend. - -.. code-block:: python - - # valid - class VanillaFilter(django_filters.FilterSet): - ... - - class DRFFilter(rest_framework_filters.FilterSet): - vanilla = rest_framework_filters.RelatedFilter(filterset=VanillaFilter, queryset=...) - - - # invalid - class DRFFilter(rest_framework_filters.FilterSet): - ... - - class VanillaFilter(django_filters.FilterSet): - drf = rest_framework_filters.RelatedFilter(filterset=DRFFilter, queryset=...) - - -Caveats & Limitations -~~~~~~~~~~~~~~~~~~~~~ - -``MultiWidget`` is incompatible -""""""""""""""""""""""""""""""" - -djangorestframework-filters is not compatible with form widgets that parse query names that differ from the filter's -attribute name. Although this only practically applies to ``MultiWidget``, it is a general limitation that affects -custom widgets that also have this behavior. Affected filters include ``RangeFilter``, ``DateTimeFromToRangeFilter``, -``DateFromToRangeFilter``, ``TimeRangeFilter``, and ``NumericRangeFilter``. - -To demonstrate the incompatiblity, take the following filterset: - -.. code-block:: python - - class PostFilter(FilterSet): - publish_date = filters.DateFromToRangeFilter() - -The above filter allows users to perform a ``range`` query on the publication date. The filter class internally uses -``MultiWidget`` to separately parse the upper and lower bound values. The incompatibility lies in that ``MultiWidget`` -appends an index to its inner widget names. Instead of parsing ``publish_date``, it expects ``publish_date_0`` and -``publish_date_1``. It is possible to fix this by including the attribute name in the querystring, although this is -not recommended. - -.. code-block:: - - ?publish_date_0=2016-01-01&publish_date_1=2016-02-01&publish_date= - -``MultiWidget`` is also discouraged since: - -* ``core-api`` field introspection fails for similar reasons -* ``_0`` and ``_1`` are less API-friendly than ``_min`` and ``_max`` - -The recommended solutions are to either: - -* Create separate filters for each of the sub-widgets (such as ``publish_date_min`` and ``publish_date_max``). -* Use a CSV-based filter such as those derived from ``BaseCSVFilter``/``BaseInFilter``/``BaseRangeFilter``. eg, - -.. code-block:: - - ?publish_date__range=2016-01-01,2016-02-01 - - -Complex Operations ------------------- - -The ``ComplexFilterBackend`` defines a custom querystring syntax and encoding process that enables the expression of -`complex queries`_. This syntax extends standard querystrings with the ability to define multiple sets of parameters -and operators for how the queries should be combined. - -.. _`complex queries`: https://docs.djangoproject.com/en/2.0/topics/db/queries/#complex-lookups-with-q-objects - ----- - -**!** Note that this feature is experimental. Bugs may be encountered, and the backend is subject to change. - ----- - -To understand the backend more fully, consider a query to find all articles that contain titles starting with either -"Who" or "What". The underlying query could be represented with the following: - -.. code-block:: python - - q1 = Article.objects.filter(title__startswith='Who') - q2 = Article.objects.filter(title__startswith='What') - return q1 | q2 - -Now consider the query, but modified with upper and lower date bounds: - -.. code-block:: python - - q1 = Article.objects.filter(title__startswith='Who').filter(publish_date__lte='2005-01-01') - q2 = Article.objects.filter(title__startswith='What').filter(publish_date__gte='2010-01-01') - return q1 | q2 - -Using just a ``FilterSet``, it is certainly feasible to represent the former query by writing a custom filter class. -However, it is less feasible with the latter query, where multiple sets of varying data types and lookups need to be -validated. In contrast, the ``ComplexFilterBackend`` can create this complex query through the arbitrary combination -of a simple filter. To support the above, the querystring needs to be created with minimal changes. Unencoded example: - -.. code-block:: - - (title__startswith=Who&publish_date__lte=2005-01-01) | (title__startswith=What&publish_date__gte=2010-01-01) - -By default, the backend combines queries with both ``&`` (AND) and ``|`` (OR), and supports unary negation ``~``. E.g., - -.. code-block:: - - (param1=value1) & (param2=value2) | ~(param3=value3) - -The backend supports both standard and complex queries. To perform complex queries, the query must be encoded and set -as the value of the ``complex_filter_param`` (defaults to ``filters``). To perform standard queries, use the backend -in the same manner as the ``RestFrameworkFilterBackend``. - - -Configuring ``ComplexFilterBackend`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Similar to other backends, ``ComplexFilterBackend`` must be added to a view's ``filter_backends`` atribute. Either add -it to the ``DEFAULT_FILTER_BACKENDS`` setting, or set it as a backend on the view class. - -.. code-block:: python - - REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ( - 'rest_framework_filters.backends.ComplexFilterBackend', - ), - } - - # or - - class MyViewSet(generics.ListAPIView): - filter_backends = (rest_framework_filters.backends.ComplexFilterBackend, ) - ... - -You may customize how queries are combined by subclassing ``ComplexFilterBackend`` and overriding the ``operators`` -attribute. ``operators`` is a map of operator symbols to functions that combine two querysets. For example, the map -can be overridden to use the ``QuerySet.intersection()`` and ``QuerySet.union()`` instead of ``&`` and ``|``. - -.. code-block:: python - - class CustomizedBackend(ComplexFilterBackend): - operators = { - '&': QuerySet.intersection, - '|': QuerySet.union, - '-': QuerySet.difference, - } - -Unary ``negation`` relies on ORM internals and may be buggy in certain circumstances. If there are issues with this -feature, it can be disabled by setting the ``negation`` attribute to ``False`` on the backend class. If you do -experience bugs, please open an issue on the `bug tracker`_. - -.. _`bug tracker`: https://github.com/philipn/django-rest-framework-filters/issues/ - - -Complex querystring encoding -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Below is the procedure for encoding a complex query: - -* Convert the query paramaters into individual querystrings. -* URL-encode the individual querystrings. -* Wrap the encoded strings in parentheses, and join with operators. -* URL-encode the entire querystring. -* Set as the value to the complex filter param (e.g., ``?filters=``). - -Note that ``filters`` is the default parameter name and can be overridden in the backend class. - - -Using the first example, these steps can be visualized as so: - -* ``title__startswith=Who``, ``title__startswith=What`` -* ``title__startswith%3DWho``, ``title__startswith%3DWhat`` -* ``(title__startswith%3DWho) | (title__startswith%3DWhat)`` -* ``%28title__startswith%253DWho%29%20%7C%20%28title__startswith%253DWhat%29`` -* ``filters=%28title__startswith%253DWho%29%20%7C%20%28title__startswith%253DWhat%29`` - - -Error handling -~~~~~~~~~~~~~~ - -``ComplexFilterBackend`` will raise any decoding errors under the complex filtering parameter name. For example, - -.. code-block:: json - - { - "filters": [ - "Invalid querystring operator. Matched: 'foo'." - ] - } - -When filtering the querysets, filterset validation errors will be collected and raised under the complex filtering -parameter name, then under the filterset's decoded querystring. For a complex query like ``(a=1&b=2) | (c=3&d=4)``, -errors would be raised like so: - -.. code-block:: json - - { - "filters": { - "a=1&b=2": { - "a": ["..."] - }, - "c=3&d=4": { - "c": ["..."] - } - } - { - - -Migrating to 1.0 ----------------- - -Backend renamed, provides new templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The backend has been renamed from ``DjangoFilterBackend`` to ``RestFrameworkFilterBackend`` and now uses its own -template paths, located under ``rest_framework_filters`` instead of ``django_filters/rest_framework``. - -To load the included templates, it is necessary to add ``rest_framework_filters`` to the ``INSTALLED_APPS`` setting. - -``RelatedFilter.queryset`` now required -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The related filterset's model is no longer used to provide the default value for ``RelatedFilter.queryset``. This -change reduces the chance of unintentionally exposing data in the rendered filter forms. You must now explicitly -provide the ``queryset`` argument, or override the ``get_queryset()`` method (see `queryset callables`_). - - -``get_filters()`` renamed to ``get_request_filters()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -django-filter has add a ``get_filters()`` classmethod to it's API, so this method has been renamed. - - -Publishing ----------- - -.. code-block:: bash - - $ pip install -U twine setuptools wheel - $ rm -rf dist/ build/ - $ python setup.py sdist bdist_wheel - $ twine upload dist/* - - -Copyright & License -------------------- - -Copyright (c) 2013-2015 Philip Neustrom & 2016-2019 Ryan P Kilby. See `LICENSE`_ for details. - -.. _`LICENSE`: https://github.com/philipn/django-rest-framework-filters/blob/master/LICENSE diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 664d48e..0000000 --- a/codecov.yml +++ /dev/null @@ -1,2 +0,0 @@ -comment: - layout: "reach" diff --git a/manage.py b/manage.py deleted file mode 100644 index 1702b3a..0000000 --- a/manage.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -# Remove the package root directory from `sys.path`, ensuring that rest_framework_filters -# is imported from the installed site packages. Used for testing the distribution. -if '--no-pkgroot' in sys.argv: - sys.argv.remove('--no-pkgroot') - package_root = sys.path.pop(0) - - import rest_framework_filters - package_dir = os.path.join(os.getcwd(), 'rest_framework_filters') - assert not rest_framework_filters.__file__.startswith(package_dir) - - sys.path.insert(0, package_root) - - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..124fa8a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[tool.poetry] +name = "django-rest-framework-filters" +version = "1.0.0" +homepage = "https://github.com/sdelements/django-rest-framework-filters" +description = "Makes it easy to filter across relationships" +authors = ["Security Compass "] +license = "MIT" +readme = "README.md" +# See https://pypi.python.org/pypi?%3Aaction=list_classifiers +classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 5 - Production/Stable', + + # Indicate who your project is intended for + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries :: Python Modules', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: MIT', + + # Supported Languages + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.12', + 'Framework :: Django', +] +packages = [ + { include = "rest_framework_filters" }, + { include = "tests", format = "sdist" }, +] +exclude = [ + "rest_framework_filters/**/tests", + "tests" +] + +[tool.poetry.dependencies] +python = "~3.12" +django = "~4.2" + +[tool.poetry.dev-dependencies] +pre-commit = "3.7.1" +# lint +black = "24.4.2" +flake8 = "7.0.0" +flake8-bandit = "4.1.1" +flake8-bugbear = "24.4.26" +flake8-docstrings = "1.7.0" +flake8-polyfill = "1.0.2" +isort = "5.13.2" +# security +bandit = "1.7.8" +# test +django-upgrade = "1.18.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.bandit] +exclude_dirs = [ + './tests/', +] diff --git a/runtests.py b/runtests.py new file mode 100644 index 0000000..cdf26b6 --- /dev/null +++ b/runtests.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +from django.core.management import execute_from_command_line + + +def runtests(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") + argv = sys.argv[:1] + ["test"] + sys.argv[1:] + execute_from_command_line(argv) + + +if __name__ == "__main__": + runtests() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e7d3b2e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,75 +0,0 @@ -[coverage:run] -branch = True -source = . -omit = .venv/*,.tox/* - -[coverage:report] -include = rest_framework_filters/* -show_missing = True - -[isort] -skip = migrations -atomic = true -line_length = 90 -include_trailing_comma = true -multi_line_output = 5 -known_third_party = django,pytz,rest_framework,django_filters -known_first_party = rest_framework_filters - -[flake8] -max_line_length = 90 -max_complexity = 10 -exclude = migrations - -extend_ignore = - A003 ; class attribute shadowing builtin - D100,D101,D102,D103,D104,D105,D106,D107 ; docstring presence - - -[metadata] -name = djangorestframework-filters -version = 1.0.0.dev2 -description = Better filtering for Django REST Framework -long_description = file: README.rst, LICENSE -license_file = LICENSE -license = MIT - -author = Philip Neustrom -maintainer = Ryan P Kilby -author_email=philipn@gmail.com -maintainer_email = kilbyr@gmail.com - -url = https://github.com/philipn/django-rest-framework-filters -project_urls = - Source=https://github.com/philipn/django-rest-framework-filters - Tracker=https://github.com/philipn/django-rest-framework-filters/issues - -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Web Environment - Framework :: Django - Framework :: Django :: 1.11 - Framework :: Django :: 2.0 - Framework :: Django :: 2.1 - Framework :: Django :: 2.2 - Framework :: Django :: 3.0 - Framework :: Django :: 3.1 - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Topic :: Internet :: WWW/HTTP - -[options] -python_requires = >=3.5 -install_requires = - djangorestframework - django-filter>=2.0 - -zip_safe = False -include_package_data = True diff --git a/setup.py b/setup.py deleted file mode 100644 index 7e87d63..0000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -from setuptools import setup, find_packages - - -setup(packages=find_packages(exclude=['tests*'])) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index ae467f8..0000000 --- a/tox.ini +++ /dev/null @@ -1,71 +0,0 @@ -[tox] -envlist = - py{35,36}-django111, - py{35,36,37}-django20, - py{35,36,37}-django21, - py{35,36,37}-django22, - py{36,37,38}-django30, - py{36,37,38}-django31, - performance, warnings, isort, lint, dist, - -[travis] -unignore_outcomes = true - -[testenv] -commands = coverage run manage.py test --exclude-tag=perf {posargs} -envdir = {toxworkdir}/venvs/{envname} -setenv = - PYTHONDONTWRITEBYTECODE=1 -deps = - coverage>=5.0 - django-crispy-forms~=1.0 - djangorestframework~=3.11.0 - django111: django-filter~=2.1.0 - django111: django~=1.11.0 - django20: django~=2.0.0 - django21: django~=2.1.0 - django22: django~=2.2.0 - django30: django~=3.0.0 - django31: django~=3.1.0 - - -[testenv:performance] -commands = python manage.py test --tag=perf {posargs} -deps = - django - djangorestframework - -[testenv:warnings] -ignore_outcome = True -commands = python -Werror manage.py test --exclude-tag=perf {posargs} -deps = - https://github.com/django/django/archive/master.tar.gz - https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz - -[testenv:isort] -commands = isort --check-only rest_framework_filters tests {posargs:--diff} -deps = isort - -[testenv:lint] -commands = flake8 rest_framework_filters tests {posargs} -deps = - flake8 - darglint - - flake8-assertive - flake8-bugbear - flake8-builtins - flake8-commas - flake8-comprehensions - flake8-docstrings - -[testenv:dist] -commands = - twine check dist/* - python manage.py test --no-pkgroot --exclude-tag=perf {posargs} -deps = - django - djangorestframework - django-crispy-forms - readme_renderer - twine From e832ee543feccedb54fdc1e5dc3c9dfef9e929ef Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Thu, 6 Jun 2024 23:41:55 -0400 Subject: [PATCH 02/10] bump version --- CHANGELOG.rst | 151 ------------ poetry.lock | 658 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 658 insertions(+), 151 deletions(-) delete mode 100644 CHANGELOG.rst create mode 100644 poetry.lock diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index a32cdbe..0000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,151 +0,0 @@ -Unreleased: ------------ - -* #242 Deprecate ``AllLookupsFilter`` -* #191 Fix ``name`` => ``field_name`` warnings - - -v0.11.1: --------- - -Fixes a packaging issue in v0.11.0 - - -v0.11.0: --------- - -This is a minor release that upgrades django-filter compatibility from ``v1.0`` -to ``v1.1``. No new functionality has been introduced. - - -v0.10.2.post0: --------------- - -* #253 Set django-filter version at 1.x-compatible releases. - - -v0.10.2: --------- - -This is a maintenance release that fixes compatibility with django-filter. - -* #189 Fix method name collision - - -v0.10.1: --------- - -This is a maintenance release that fixes the following bugs: - -* #172 Prevent deepcopying of filter's parent - - -v0.10.0: --------- - -This release primarily adds compatibility with django-filter 1.0 (more details -in #144), and is an intermediate step to overhauling the behavior of filters -that span relationships. - -As `RelatedFilter` is a subclass of `ModelChoiceFilter`, you may take advantage -of the `callable` behavior for the `queryset` argument. The `queryset` is now a -required argument, which is a forwards-incompatible change. You can provide the -model's default queryset to maintain the current behavior, or a callable, which -will allow you to filter the queryset by the request's properties. - -* #124 Removed deprecation warnings -* #128 Fix all lookups handling for related fields -* #129 Fix template rendering -* #139 Fix metaclass inheritance bug -* #146 Make `RelatedFilter.queryset` a required argument -* #154 Add python 3.6 support -* #161 Fix request-based filtering -* #170 Improve RelatedFilter queryset error message - -v0.9.1: -------- - -* #128 Fix all lookups handling for related fields -* #129 Fix backend template rendering -* #148 Version lock django-filter<1.0 due to API incompatibilities - -v0.9.0: -------- - -This release is tied to the 0.15.0 update of django-filter, and is in preparation of -a (near) simultaneous 1.0 release. All current deprecations will be removed in the -next release. - -* Updates django-filter requirement to 0.15.0 -* #101 Add support for Django 1.10, set DRF support to 3.3, 3.4, and drop support for python 3.2 -* #114 Add ``lookups`` argument to ``RelatedFilter`` -* #113 Deprecated ``MethodFilter`` for new ``Filter.method`` argument -* #123 Fix declared filters being overwritten by ``AllLookupsFilter`` - -v0.8.1: -------- - -* Fix bug where AllLookupsFilter would override a declared filter of the same name -* #84 Fix AllLookupsFilter compatibility with ``ForeignObject`` related fields -* #82 Fix AllLookupsFilter compatibility with mixin FilterSets -* #81 Fix bug where FilterSet modified ``ViewSet.filter_fields`` -* #79 Prevent infinite recursion for chainable transforms, fixing compatiblity - w/ ``django.contrib.postgres`` - -v0.8.0: -------- - -This release is tied to a major update of django-filter (more details in #66), -which fixes how lookup expressions are resolved. 'in', 'range', and 'isnull' -lookups no longer require special handling by django-rest-framework-filters. -This has the following effects: - - * Deprecates ArrayDecimalField/InSetNumberFilter - * Deprecates ArrayCharField/InSetCharFilter - * Deprecates FilterSet.fix_filter_field - * Deprecates ALL_LOOKUPS in favor of '__all__' constant - * AllLookupsFilter now generates only valid lookup expressions - -* #2 'range' lookup types do not work -* #15 Date lookup types do not work (year, day, ...) -* #16 'in' lookup types do not work -* #64 Fix browsable API filter form -* #69 Fix compatibility with base django-filter `FilterSet`s -* #70 Refactor related filter handling, fixing some edge cases -* Deprecated 'cache' argument to FilterSet -* #73 Warn use of `order_by` - -v0.7.0: -------- - -* #61 Change django-filter requirement to 0.12.0 -* Adds support for Django 1.9 -* #47 Changes implementation of MethodFilterss -* Drops support for Django 1.7 -* #49 Fix ALL_LOOKUPS shortcut to obey filter overrides (in, isnull) -* #46 Fix boolean filter behavior (#25) and isnull override (#6) -* #60 Fix filtering on nonexistent related field - -v0.6.0: -------- - -* #43 Adds a filter exclusion/negation syntax. eg, ?some_filter!=some_value -* #44 Sets the minimum django-filter version required - -v0.5.0: -------- - -* #38 Rework of related filtering, improving performance (#8) and some minor correctness issues -* #35 Add ALL_LOOKUPS shortcut for dict-style filter definitions -* #31 Fix timezone-aware datetime handling -* #36 Fix '__in' filter to work with strings -* #33 Fix RelatedFilter handling to not override existing isnull filters -* #35 Fix python 3.5 compatibility issue -* Drops support for Django 1.6 and below - -v0.4.0: -------- - -* Adds support for Django 1.8, DRF 3.2 -* Drops support for Python 2.6, DRF 2.x -* #23 Adds __in filtering for numeric field types. eg, ?id__in=1,2,3 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..e55a897 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,658 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "bandit" +version = "1.7.8" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bandit-1.7.8-py3-none-any.whl", hash = "sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381"}, + {file = "bandit-1.7.8.tar.gz", hash = "sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0)"] +yaml = ["PyYAML"] + +[[package]] +name = "black" +version = "24.4.2" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "django" +version = "4.2.13" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"}, + {file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-upgrade" +version = "1.18.0" +description = "Automatically upgrade your Django project code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_upgrade-1.18.0-py3-none-any.whl", hash = "sha256:bae6a466bb9dd63dd7e23b665499b84499b2661348f06b371b88d2e609aa0df9"}, + {file = "django_upgrade-1.18.0.tar.gz", hash = "sha256:ae2a2de13e7804773201aef6af2245fa5d503b0a7c88b85b12cf1fdb84197065"}, +] + +[package.dependencies] +tokenize-rt = ">=4.1" + +[[package]] +name = "filelock" +version = "3.14.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, + {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "flake8-bandit" +version = "4.1.1" +description = "Automated security testing with bandit and flake8." +optional = false +python-versions = ">=3.6" +files = [ + {file = "flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d"}, + {file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"}, +] + +[package.dependencies] +bandit = ">=1.7.3" +flake8 = ">=5.0.0" + +[[package]] +name = "flake8-bugbear" +version = "24.4.26" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8_bugbear-24.4.26-py3-none-any.whl", hash = "sha256:cb430dd86bc821d79ccc0b030789a9c87a47a369667f12ba06e80f11305e8258"}, + {file = "flake8_bugbear-24.4.26.tar.gz", hash = "sha256:ff8d4ba5719019ebf98e754624c30c05cef0dadcf18a65d91c7567300e52a130"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=6.0.0" + +[package.extras] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, + {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, +] + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "flake8-polyfill" +version = "1.0.2" +description = "Polyfill package for Flake8 plugins" +optional = false +python-versions = "*" +files = [ + {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, + {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, +] + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "identify" +version = "2.5.36" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pbr" +version = "6.0.0" +description = "Python Build Reasonableness" +optional = false +python-versions = ">=2.6" +files = [ + {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, + {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pre-commit" +version = "3.7.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.0" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, + {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "stevedore" +version = "5.2.0" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, + {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, +] + +[package.dependencies] +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "tokenize-rt" +version = "5.2.0" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"}, + {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, + {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[metadata] +lock-version = "2.0" +python-versions = "~3.12" +content-hash = "b56e5369f55e0037bbbb0078b5024b35d7ae0883c1985928ca8cdb47ecfeb262" From 26335fca204c214ab223c35b8fc84b64ce249bb4 Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Fri, 7 Jun 2024 00:04:48 -0400 Subject: [PATCH 03/10] django fixes --- poetry.lock | 59 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 6 +++- tests/perf/urls.py | 4 +-- tests/related/data.py | 2 +- tests/settings.py | 6 ++++ tests/test_complex_ops.py | 8 +++--- tests/test_filtering.py | 8 +++--- tests/testapp/__init__.py | 1 - tests/testapp/urls.py | 4 +-- 9 files changed, 82 insertions(+), 16 deletions(-) diff --git a/poetry.lock b/poetry.lock index e55a897..a4d7111 100644 --- a/poetry.lock +++ b/poetry.lock @@ -137,6 +137,21 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "crispy-bootstrap3" +version = "2024.1" +description = "Bootstrap3 template pack for django-crispy-forms" +optional = false +python-versions = ">=3.7" +files = [ + {file = "crispy-bootstrap3-2024.1.tar.gz", hash = "sha256:343c696ae1a854ac0ccad25e9e7d782400783034220a11aa179d1d799acf6161"}, + {file = "crispy_bootstrap3-2024.1-py3-none-any.whl", hash = "sha256:257555c61ec6cd792e8654822e836794237465442a6e4b47ed31f7464e8c10f4"}, +] + +[package.dependencies] +django = ">=3.2" +django-crispy-forms = ">=1.14.0" + [[package]] name = "distlib" version = "0.3.8" @@ -168,6 +183,34 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-crispy-forms" +version = "2.1" +description = "Best way to have Django DRY forms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-crispy-forms-2.1.tar.gz", hash = "sha256:4d7ec431933ad4d4b5c5a6de4a584d24613c347db9ac168723c9aaf63af4bb96"}, + {file = "django_crispy_forms-2.1-py3-none-any.whl", hash = "sha256:d592044771412ae1bd539cc377203aa61d4eebe77fcbc07fbc8f12d3746d4f6b"}, +] + +[package.dependencies] +django = ">=4.2" + +[[package]] +name = "django-filter" +version = "24.2" +description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, + {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, +] + +[package.dependencies] +Django = ">=4.2" + [[package]] name = "django-upgrade" version = "1.18.0" @@ -182,6 +225,20 @@ files = [ [package.dependencies] tokenize-rt = ">=4.1" +[[package]] +name = "djangorestframework" +version = "3.15.1" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, + {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, +] + +[package.dependencies] +django = ">=3.0" + [[package]] name = "filelock" version = "3.14.0" @@ -655,4 +712,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "~3.12" -content-hash = "b56e5369f55e0037bbbb0078b5024b35d7ae0883c1985928ca8cdb47ecfeb262" +content-hash = "59339a5492b47c70acae956489d2ab384d0218fd771f15855f64cf3b1ab8fa62" diff --git a/pyproject.toml b/pyproject.toml index 124fa8a..1b0430d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,11 @@ exclude = [ [tool.poetry.dependencies] python = "~3.12" -django = "~4.2" +django = "^4.2" +django-filter = "^24.2" +djangorestframework = "^3.15" +django-crispy-forms = "^2.1" +crispy-bootstrap3 = "^2024.1" [tool.poetry.dev-dependencies] pre-commit = "3.7.1" diff --git a/tests/perf/urls.py b/tests/perf/urls.py index f253106..8b5da66 100644 --- a/tests/perf/urls.py +++ b/tests/perf/urls.py @@ -1,5 +1,5 @@ -from django.conf.urls import include, url +from django.urls import include, path from rest_framework import routers from . import views @@ -10,5 +10,5 @@ urlpatterns = [ - url(r'^', include(router.urls)), + path('', include(router.urls)), ] diff --git a/tests/related/data.py b/tests/related/data.py index b29cbb8..6103d2e 100644 --- a/tests/related/data.py +++ b/tests/related/data.py @@ -102,4 +102,4 @@ def postD(cls, blog): ) def verify(self, qs, expected): - self.assertQuerysetEqual(qs, expected, attrgetter('pk'), False) + self.assertQuerySetEqual(qs, expected, attrgetter('pk'), False) diff --git a/tests/settings.py b/tests/settings.py index 5729fa5..4237ba4 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -21,6 +21,8 @@ 'rest_framework_filters', 'rest_framework', 'django_filters', + "crispy_forms", + "crispy_bootstrap3", 'tests.testapp', ) @@ -47,3 +49,7 @@ ROOT_URLCONF = 'tests.testapp.urls' STATIC_URL = '/static/' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +CRISPY_TEMPLATE_PACK = 'bootstrap3' diff --git a/tests/test_complex_ops.py b/tests/test_complex_ops.py index c88ad44..20f7466 100644 --- a/tests/test_complex_ops.py +++ b/tests/test_complex_ops.py @@ -184,7 +184,7 @@ def test_single(self): querysets = [models.User.objects.filter(first_name='Bob')] complex_ops = [ComplexOp(None, False, None)] - self.assertQuerysetEqual( + self.assertQuerySetEqual( combine_complex_queryset(querysets, complex_ops), ['u1', 'u3'], attrgetter('username'), False, ) @@ -199,7 +199,7 @@ def test_AND(self): ComplexOp(None, False, None), ] - self.assertQuerysetEqual( + self.assertQuerySetEqual( combine_complex_queryset(querysets, complex_ops), ['u1'], attrgetter('username'), False, ) @@ -214,7 +214,7 @@ def test_OR(self): ComplexOp(None, False, None), ] - self.assertQuerysetEqual( + self.assertQuerySetEqual( combine_complex_queryset(querysets, complex_ops), ['u1', 'u3', 'u4'], attrgetter('username'), False, ) @@ -229,7 +229,7 @@ def test_negation(self): ComplexOp(None, True, None), ] - self.assertQuerysetEqual( + self.assertQuerySetEqual( combine_complex_queryset(querysets, complex_ops), ['u1'], attrgetter('username'), False, ) diff --git a/tests/test_filtering.py b/tests/test_filtering.py index dfa7291..337c01f 100644 --- a/tests/test_filtering.py +++ b/tests/test_filtering.py @@ -311,10 +311,10 @@ def test_relatedfilter_for_aliased_nested_relationships(self): f3 = PageFilter({'two_pages_back': '3'}, queryset=qs) f4 = PageFilter({'two_pages_back': '4'}, queryset=qs) - self.assertQuerysetEqual(f1.qs, [3], lambda p: p.pk) - self.assertQuerysetEqual(f2.qs, [4], lambda p: p.pk) - self.assertQuerysetEqual(f3.qs, [], lambda p: p.pk) - self.assertQuerysetEqual(f4.qs, [], lambda p: p.pk) + self.assertQuerySetEqual(f1.qs, [3], lambda p: p.pk) + self.assertQuerySetEqual(f2.qs, [4], lambda p: p.pk) + self.assertQuerySetEqual(f3.qs, [], lambda p: p.pk) + self.assertQuerySetEqual(f4.qs, [], lambda p: p.pk) def test_relatedfilter_different_name(self): # Test the name filter on the related UserFilter set. diff --git a/tests/testapp/__init__.py b/tests/testapp/__init__.py index d6c0860..8b13789 100644 --- a/tests/testapp/__init__.py +++ b/tests/testapp/__init__.py @@ -1,2 +1 @@ -default_app_config = 'tests.testapp.apps.TestappConfig' diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py index 3d3429c..8bc29d9 100644 --- a/tests/testapp/urls.py +++ b/tests/testapp/urls.py @@ -1,5 +1,5 @@ -from django.conf.urls import include, url +from django.urls import include, path from rest_framework import routers from . import views @@ -15,5 +15,5 @@ urlpatterns = [ - url(r'^', include(router.urls)), + path('', include(router.urls)), ] From 4b7a635f66743a508a8c7a1c5c95ed8c1e8f3d9c Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Fri, 7 Jun 2024 00:53:55 -0400 Subject: [PATCH 04/10] remove crispy forms --- poetry.lock | 31 +------- pyproject.toml | 2 - rest_framework_filters/backends.py | 2 - rest_framework_filters/filterset.py | 15 ---- .../rest_framework_filters/crispy_form.html | 18 ----- tests/settings.py | 4 - tests/test_backends.py | 73 ------------------- 7 files changed, 1 insertion(+), 144 deletions(-) delete mode 100644 rest_framework_filters/templates/rest_framework_filters/crispy_form.html diff --git a/poetry.lock b/poetry.lock index a4d7111..1f924f0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -137,21 +137,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "crispy-bootstrap3" -version = "2024.1" -description = "Bootstrap3 template pack for django-crispy-forms" -optional = false -python-versions = ">=3.7" -files = [ - {file = "crispy-bootstrap3-2024.1.tar.gz", hash = "sha256:343c696ae1a854ac0ccad25e9e7d782400783034220a11aa179d1d799acf6161"}, - {file = "crispy_bootstrap3-2024.1-py3-none-any.whl", hash = "sha256:257555c61ec6cd792e8654822e836794237465442a6e4b47ed31f7464e8c10f4"}, -] - -[package.dependencies] -django = ">=3.2" -django-crispy-forms = ">=1.14.0" - [[package]] name = "distlib" version = "0.3.8" @@ -183,20 +168,6 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] -[[package]] -name = "django-crispy-forms" -version = "2.1" -description = "Best way to have Django DRY forms" -optional = false -python-versions = ">=3.8" -files = [ - {file = "django-crispy-forms-2.1.tar.gz", hash = "sha256:4d7ec431933ad4d4b5c5a6de4a584d24613c347db9ac168723c9aaf63af4bb96"}, - {file = "django_crispy_forms-2.1-py3-none-any.whl", hash = "sha256:d592044771412ae1bd539cc377203aa61d4eebe77fcbc07fbc8f12d3746d4f6b"}, -] - -[package.dependencies] -django = ">=4.2" - [[package]] name = "django-filter" version = "24.2" @@ -712,4 +683,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "~3.12" -content-hash = "59339a5492b47c70acae956489d2ab384d0218fd771f15855f64cf3b1ab8fa62" +content-hash = "b28efeabdf6b4dafcd5de6fcc25fb80c7b04141d1b2c415b79cd5f2af2773bdb" diff --git a/pyproject.toml b/pyproject.toml index 1b0430d..9baa145 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,8 +42,6 @@ python = "~3.12" django = "^4.2" django-filter = "^24.2" djangorestframework = "^3.15" -django-crispy-forms = "^2.1" -crispy-bootstrap3 = "^2024.1" [tool.poetry.dev-dependencies] pre-commit = "3.7.1" diff --git a/rest_framework_filters/backends.py b/rest_framework_filters/backends.py index a8476a7..1b3e909 100644 --- a/rest_framework_filters/backends.py +++ b/rest_framework_filters/backends.py @@ -14,8 +14,6 @@ class RestFrameworkFilterBackend(backends.DjangoFilterBackend): @property def template(self): - if compat.is_crispy(): - return 'rest_framework_filters/crispy_form.html' return 'rest_framework_filters/form.html' @contextmanager diff --git a/rest_framework_filters/filterset.py b/rest_framework_filters/filterset.py index b52d25e..e57cdf0 100644 --- a/rest_framework_filters/filterset.py +++ b/rest_framework_filters/filterset.py @@ -377,18 +377,3 @@ def clean(form): return cleaned_data return Form - - @property - def form(self): - from django_filters import compat - - form = super().form - if compat.is_crispy(): - from crispy_forms.helper import FormHelper - - form.helper = FormHelper(form) - form.helper.form_tag = False - form.helper.disable_csrf = True - form.helper.template_pack = 'bootstrap3' - - return form diff --git a/rest_framework_filters/templates/rest_framework_filters/crispy_form.html b/rest_framework_filters/templates/rest_framework_filters/crispy_form.html deleted file mode 100644 index 2d5a6f8..0000000 --- a/rest_framework_filters/templates/rest_framework_filters/crispy_form.html +++ /dev/null @@ -1,18 +0,0 @@ -{% load rest_framework_filters %} -{% load crispy_forms_tags %} -{% load i18n %} - -

{% trans "Field filters" %}

-
- {% crispy filter.form %} - - {% for related_name, filterset in filter.related_filtersets.items %} -
- {{ filter|label:related_name }} - - {% crispy filterset.form %} -
- {% endfor %} - - -
diff --git a/tests/settings.py b/tests/settings.py index 4237ba4..c82bbd2 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -21,8 +21,6 @@ 'rest_framework_filters', 'rest_framework', 'django_filters', - "crispy_forms", - "crispy_bootstrap3", 'tests.testapp', ) @@ -51,5 +49,3 @@ STATIC_URL = '/static/' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -CRISPY_TEMPLATE_PACK = 'bootstrap3' diff --git a/tests/test_backends.py b/tests/test_backends.py index 9083df6..672a4f8 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -331,79 +331,6 @@ def test_patch_for_rendering_handles_exception(self): self.assertEqual(backend.get_filterset_class, original) -@modify_settings(INSTALLED_APPS={'append': ['crispy_forms']}) -class BackendCrispyFormsRenderingTests(RenderMixin, APITestCase): - - def test_crispy_forms_filterset_compatibility(self): - class SimpleCrispyFilterSet(FilterSet): - class Meta: - model = models.User - fields = ['username'] - - class SimpleViewSet(views.FilterFieldsUserViewSet): - filterset_class = SimpleCrispyFilterSet - - self.assertHTMLEqual(self.render(SimpleViewSet), """ -

Field filters

-
-
- -
- -
-
- -
- """) - - def test_related_filterset_crispy_forms(self): - class UserFilter(FilterSet): - username = filters.CharFilter() - - class NoteFilter(FilterSet): - author = filters.RelatedFilter( - filterset=UserFilter, - queryset=models.User.objects.all(), - label='Writer', - ) - - class RelatedViewSet(views.NoteViewSet): - filterset_class = NoteFilter - - self.assertHTMLEqual(self.render(RelatedViewSet), """ -

Field filters

-
-
- -
- -
-
- -
- Writer - -
- -
- -
-
-
- - -
- """) - - class ComplexFilterBackendTests(APITestCase): @classmethod From 482dd61e61b23984647093f10200d09f1587cccb Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Fri, 7 Jun 2024 00:56:37 -0400 Subject: [PATCH 05/10] linting fixes --- Makefile | 2 +- rest_framework_filters/backends.py | 1 - rest_framework_filters/filters.py | 10 ++++++---- rest_framework_filters/utils.py | 2 +- tests/settings.py | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 1515db5..fd11ff1 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ lint: - poetry run flake8 . --extend-ignore=D,E501,W601 --extend-exclude=docs/ --statistics --count + poetry run flake8 . --extend-ignore=D,E501,W601 --extend-exclude=docs/,tests/ --statistics --count test: poetry run python ./runtests.py diff --git a/rest_framework_filters/backends.py b/rest_framework_filters/backends.py index 1b3e909..4eea538 100644 --- a/rest_framework_filters/backends.py +++ b/rest_framework_filters/backends.py @@ -1,7 +1,6 @@ from contextlib import contextmanager from django.http import QueryDict -from django_filters import compat from django_filters.rest_framework import backends from rest_framework.exceptions import ValidationError diff --git a/rest_framework_filters/filters.py b/rest_framework_filters/filters.py index 09f4727..fd2d09d 100644 --- a/rest_framework_filters/filters.py +++ b/rest_framework_filters/filters.py @@ -99,10 +99,12 @@ def fset(self, value): def get_queryset(self, request): queryset = super(BaseRelatedFilter, self).get_queryset(request) - assert queryset is not None, \ - "Expected `.get_queryset()` for related filter '%s.%s' to " \ - "return a `QuerySet`, but got `None`." \ - % (self.parent.__class__.__name__, self.field_name) + if queryset is None: + raise ValueError( + "Expected `.get_queryset()` for related filter '%s.%s' to return a `QuerySet`, but got `None`." + % (self.parent.__class__.__name__, self.field_name) + ) + return queryset diff --git a/rest_framework_filters/utils.py b/rest_framework_filters/utils.py index e713234..d1100b0 100644 --- a/rest_framework_filters/utils.py +++ b/rest_framework_filters/utils.py @@ -51,7 +51,7 @@ def lookups_for_transform(transform): if issubclass(lookup, Transform): # type match indicates recursion. - if type(transform) == lookup: + if type(transform) is lookup: continue sub_transform = lookup(transform) diff --git a/tests/settings.py b/tests/settings.py index c82bbd2..48fa3ef 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -24,7 +24,7 @@ 'tests.testapp', ) -SECRET_KEY = 'testsecretkey' +SECRET_KEY = 'testsecretkey' # noqa TEMPLATES = [ { From fb2d1723d3eff31879a4eb30d67ef6bb48c9bc0d Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Fri, 7 Jun 2024 01:00:44 -0400 Subject: [PATCH 06/10] fix errors in ci --- .github/workflows/validate.yml | 8 +++----- Makefile | 7 ++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c3910ba..0a59d99 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -30,10 +30,8 @@ jobs: poetry install - name: Linting run: | - poetry run isort . - poetry run black . - poetry run flake8 . --extend-ignore=D,E501,W601 --extend-exclude=docs/ --statistics --count + make lint - name: Security - run: poetry run bandit -c pyproject.toml -r . + run: make bandit - name: Testing - run: poetry run python ./runtests.py + run: make tests diff --git a/Makefile b/Makefile index fd11ff1..2dbf2c5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,10 @@ lint: - poetry run flake8 . --extend-ignore=D,E501,W601 --extend-exclude=docs/,tests/ --statistics --count + poetry run black . + poetry run isort . + poetry run flake8 . --extend-ignore=D,E501,W601 --extend-exclude=docs/,**/migrations/*,**/south_migrations/*,tests/ --statistics --count + +bandit: + poetry run bandit -c pyproject.toml -r . test: poetry run python ./runtests.py From e12f7b82a2892db419a507beaf1a3abcde88c26e Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Fri, 7 Jun 2024 01:19:24 -0400 Subject: [PATCH 07/10] test fixes --- rest_framework_filters/filters.py | 1 - tests/test_filtering.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/rest_framework_filters/filters.py b/rest_framework_filters/filters.py index fd2d09d..82908e2 100644 --- a/rest_framework_filters/filters.py +++ b/rest_framework_filters/filters.py @@ -104,7 +104,6 @@ def get_queryset(self, request): "Expected `.get_queryset()` for related filter '%s.%s' to return a `QuerySet`, but got `None`." % (self.parent.__class__.__name__, self.field_name) ) - return queryset diff --git a/tests/test_filtering.py b/tests/test_filtering.py index 337c01f..888e30c 100644 --- a/tests/test_filtering.py +++ b/tests/test_filtering.py @@ -442,7 +442,7 @@ class Meta: GET = {'author': User.objects.get(username='user2').pk} msg = "Expected `.get_queryset()` for related filter 'NoteFilter.author' " \ "to return a `QuerySet`, but got `None`." - with self.assertRaisesMessage(AssertionError, msg): + with self.assertRaisesMessage(ValueError, msg): NoteFilter(GET, queryset=Note.objects.all()) def test_relatedfilter_request_is_passed(self): From 438e584713b6315c1ea7b89b2f9a7406a0fb76d5 Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Fri, 7 Jun 2024 01:20:00 -0400 Subject: [PATCH 08/10] linting fixes --- rest_framework_filters/backends.py | 8 +- rest_framework_filters/complex_ops.py | 24 +- rest_framework_filters/filters.py | 12 +- rest_framework_filters/filterset.py | 45 +-- rest_framework_filters/utils.py | 8 +- tests/perf/filters.py | 19 +- tests/perf/serializers.py | 3 +- tests/perf/tests.py | 82 +++-- tests/perf/urls.py | 7 +- tests/related/data.py | 48 +-- tests/related/test_exclude.py | 58 ++-- tests/related/test_filter.py | 34 +- tests/settings.py | 54 ++- tests/test_backends.py | 154 +++++---- tests/test_complex_ops.py | 169 ++++++---- tests/test_filtering.py | 262 ++++++++------- tests/test_filters.py | 12 +- tests/test_filterset.py | 408 ++++++++++++----------- tests/test_forms.py | 45 +-- tests/test_regressions.py | 166 ++++----- tests/test_utils.py | 43 +-- tests/testapp/apps.py | 3 +- tests/testapp/filters.py | 81 +++-- tests/testapp/lookups.py | 4 +- tests/testapp/migrations/0001_initial.py | 292 ++++++++++++---- tests/testapp/models.py | 28 +- tests/testapp/serializers.py | 5 +- tests/testapp/urls.py | 17 +- tests/testapp/views.py | 15 +- 29 files changed, 1201 insertions(+), 905 deletions(-) diff --git a/rest_framework_filters/backends.py b/rest_framework_filters/backends.py index 4eea538..85dca7a 100644 --- a/rest_framework_filters/backends.py +++ b/rest_framework_filters/backends.py @@ -13,7 +13,7 @@ class RestFrameworkFilterBackend(backends.DjangoFilterBackend): @property def template(self): - return 'rest_framework_filters/form.html' + return "rest_framework_filters/form.html" @contextmanager def patch_for_rendering(self, request): @@ -48,7 +48,7 @@ def to_html(self, request, queryset, view): class ComplexFilterBackend(RestFrameworkFilterBackend): - complex_filter_param = 'filters' + complex_filter_param = "filters" operators = None negation = True @@ -70,7 +70,9 @@ def filter_queryset(self, request, queryset, view): # Collect the individual filtered querysets querystrings = [op.querystring for op in complex_ops] try: - querysets = self.get_filtered_querysets(querystrings, request, queryset, view) + querysets = self.get_filtered_querysets( + querystrings, request, queryset, view + ) except ValidationError as exc: raise ValidationError({self.complex_filter_param: exc.detail}) diff --git a/rest_framework_filters/complex_ops.py b/rest_framework_filters/complex_ops.py index f7e9814..bb19b41 100644 --- a/rest_framework_filters/complex_ops.py +++ b/rest_framework_filters/complex_ops.py @@ -12,14 +12,14 @@ # current iteration: https://regex101.com/r/5rPycz/3 # special thanks to @JohnDoe2 on the #regex IRC channel! # matches groups of "()" -COMPLEX_OP_RE = re.compile(r'()\(([^)]+)\)([^(]*?(?=\())?') -COMPLEX_OP_NEG_RE = re.compile(r'(~?)\(([^)]+)\)([^(]*?(?=~\(|\())?') +COMPLEX_OP_RE = re.compile(r"()\(([^)]+)\)([^(]*?(?=\())?") +COMPLEX_OP_NEG_RE = re.compile(r"(~?)\(([^)]+)\)([^(]*?(?=~\(|\())?") COMPLEX_OPERATORS = { - '&': QuerySet.__and__, - '|': QuerySet.__or__, + "&": QuerySet.__and__, + "|": QuerySet.__or__, } -ComplexOp = namedtuple('ComplexOp', ['querystring', 'negate', 'op']) +ComplexOp = namedtuple("ComplexOp", ["querystring", "negate", "op"]) def decode_complex_ops(encoded_querystring, operators=None, negation=True): @@ -61,25 +61,27 @@ def decode_complex_ops(encoded_querystring, operators=None, negation=True): if not matches: msg = _("Unable to parse querystring. Decoded: '%(decoded)s'.") - raise ValidationError(msg % {'decoded': decoded_querystring}) + raise ValidationError(msg % {"decoded": decoded_querystring}) results, errors = [], [] for match, has_next in lookahead(matches): negate, querystring, op = match.groups() - negate = negate == '~' + negate = negate == "~" querystring = unquote(querystring) op_func = operators.get(op.strip()) if op else None if op_func is None and has_next: msg = _("Invalid querystring operator. Matched: '%(op)s'.") - errors.append(msg % {'op': op}) + errors.append(msg % {"op": op}) results.append(ComplexOp(querystring, negate, op_func)) - msg = _("Ending querystring must not have trailing characters. Matched: '%(chars)s'.") - trailing_chars = decoded_querystring[matches[-1].end():] + msg = _( + "Ending querystring must not have trailing characters. Matched: '%(chars)s'." + ) + trailing_chars = decoded_querystring[matches[-1].end() :] # noqa: E203 if trailing_chars: - errors.append(msg % {'chars': trailing_chars}) + errors.append(msg % {"chars": trailing_chars}) if errors: raise ValidationError(errors) diff --git a/rest_framework_filters/filters.py b/rest_framework_filters/filters.py index 82908e2..6283091 100644 --- a/rest_framework_filters/filters.py +++ b/rest_framework_filters/filters.py @@ -2,11 +2,10 @@ from django.utils.module_loading import import_string from django_filters.rest_framework.filters import * # noqa -from django_filters.rest_framework.filters import ( - ModelChoiceFilter, ModelMultipleChoiceFilter, -) +from django_filters.rest_framework.filters import (ModelChoiceFilter, + ModelMultipleChoiceFilter) -ALL_LOOKUPS = '__all__' +ALL_LOOKUPS = "__all__" class AutoFilter: @@ -76,7 +75,7 @@ class creation time, instead of during initialization. Args: filterset: The filterset to bind """ - if not hasattr(self, 'bound_filterset'): + if not hasattr(self, "bound_filterset"): self.bound_filterset = filterset def filterset(): @@ -87,7 +86,7 @@ def fget(self): self._filterset = import_string(self._filterset) except ImportError: # Fallback to building import path relative to bind class - path = '.'.join([self.bound_filterset.__module__, self._filterset]) + path = ".".join([self.bound_filterset.__module__, self._filterset]) self._filterset = import_string(path) return self._filterset @@ -95,6 +94,7 @@ def fset(self, value): self._filterset = value return locals() + filterset = property(**filterset()) def get_queryset(self, request): diff --git a/rest_framework_filters/filterset.py b/rest_framework_filters/filterset.py index e57cdf0..70ee609 100644 --- a/rest_framework_filters/filterset.py +++ b/rest_framework_filters/filterset.py @@ -17,13 +17,17 @@ def related(filterset, filter_name): class FilterSetMetaclass(filterset.FilterSetMetaclass): def __new__(cls, name, bases, attrs): - attrs['auto_filters'] = cls.get_auto_filters(bases, attrs) + attrs["auto_filters"] = cls.get_auto_filters(bases, attrs) new_class = super().__new__(cls, name, bases, attrs) - new_class.related_filters = OrderedDict([ - (name, f) for name, f in new_class.declared_filters.items() - if isinstance(f, filters.BaseRelatedFilter)]) + new_class.related_filters = OrderedDict( + [ + (name, f) + for name, f in new_class.declared_filters.items() + if isinstance(f, filters.BaseRelatedFilter) + ] + ) # See: :meth:`rest_framework_filters.filters.RelatedFilter.bind` for f in new_class.related_filters.values(): @@ -54,17 +58,17 @@ def get_auto_filters(cls, bases, attrs): # Default the `filter.field_name` to the attribute name on the filterset for filter_name, f in auto_filters: - if getattr(f, 'field_name', None) is None: + if getattr(f, "field_name", None) is None: f.field_name = filter_name auto_filters.sort(key=lambda x: x[1].creation_counter) # merge auto filters from base classes for base in reversed(bases): - if hasattr(base, 'auto_filters'): + if hasattr(base, "auto_filters"): auto_filters = [ - (name, f) for name, f - in base.auto_filters.items() + (name, f) + for name, f in base.auto_filters.items() if name not in attrs ] + auto_filters @@ -108,8 +112,9 @@ def expand_auto_filter(cls, new_class, filter_name, f): if gen_f.lookup_expr != "exact": # Update field name to also include lookup expr. - gen_f.field_name = "{field_name}__{lookup_expr}".format(field_name=f.field_name, - lookup_expr=gen_f.lookup_expr) + gen_f.field_name = "{field_name}__{lookup_expr}".format( + field_name=f.field_name, lookup_expr=gen_f.lookup_expr + ) # do not overwrite declared filters if gen_name not in orig_declared: @@ -197,8 +202,9 @@ def disable_subset(cls, *, depth=0): This filterset class with subset disabling mixed in. """ if not issubclass(cls, SubsetDisabledMixin): - cls = type('SubsetDisabled%s' % cls.__name__, - (SubsetDisabledMixin, cls), {}) + cls = type( + "SubsetDisabled%s" % cls.__name__, (SubsetDisabledMixin, cls), {} + ) # recursively disable subset for related filtersets if depth > 0: @@ -254,16 +260,16 @@ def get_param_filter_name(cls, param, rel=None): return None # strip the rel prefix from the param name. - prefix = '%s%s' % (rel or '', LOOKUP_SEP) + prefix = "%s%s" % (rel or "", LOOKUP_SEP) if rel and param.startswith(prefix): - param = param[len(prefix):] + param = param[len(prefix) :] # noqa: E203 # Attempt to match against filters with lookups first. (username__endswith) if param in cls.base_filters: return param # Attempt to match against exclusion filters - if param[-1] == '!' and param[:-1] in cls.base_filters: + if param[-1] == "!" and param[:-1] in cls.base_filters: return param[:-1] # Match against relationships. (author__username__endswith). @@ -288,7 +294,7 @@ def get_request_filters(self): requested_filters[filter_name] = f # exclusion params - exclude_name = '%s!' % filter_name + exclude_name = "%s!" % filter_name if related(self, exclude_name) in self.data: # deepcopy the *base* filter to prevent copying of model & parent f_copy = copy.deepcopy(self.base_filters[filter_name]) @@ -341,15 +347,15 @@ def filter_related_filtersets(self, queryset): """ for related_name, related_filterset in self.related_filtersets.items(): # Related filtersets should only be applied if they had data. - prefix = '%s%s' % (related(self, related_name), LOOKUP_SEP) + prefix = "%s%s" % (related(self, related_name), LOOKUP_SEP) if not any(value.startswith(prefix) for value in self.data): continue field = self.filters[related_name].field - to_field_name = getattr(field, 'to_field_name', 'pk') or 'pk' + to_field_name = getattr(field, "to_field_name", "pk") or "pk" field_name = self.filters[related_name].field_name - lookup_expr = LOOKUP_SEP.join([field_name, 'in']) + lookup_expr = LOOKUP_SEP.join([field_name, "in"]) subquery = related_filterset.qs.values(to_field_name) queryset = queryset.filter(**{lookup_expr: subquery}) @@ -376,4 +382,5 @@ def clean(form): self.form.errors[related(related_filterset, key)] = error return cleaned_data + return Form diff --git a/rest_framework_filters/utils.py b/rest_framework_filters/utils.py index d1100b0..bff4b34 100644 --- a/rest_framework_filters/utils.py +++ b/rest_framework_filters/utils.py @@ -18,8 +18,8 @@ def lookups_for_field(model_field): if issubclass(lookup, Transform): transform = lookup(Expression(model_field)) lookups += [ - LOOKUP_SEP.join([expr, sub_expr]) for sub_expr - in lookups_for_transform(transform) + LOOKUP_SEP.join([expr, sub_expr]) + for sub_expr in lookups_for_transform(transform) ] else: @@ -56,8 +56,8 @@ def lookups_for_transform(transform): sub_transform = lookup(transform) lookups += [ - LOOKUP_SEP.join([expr, sub_expr]) for sub_expr - in lookups_for_transform(sub_transform) + LOOKUP_SEP.join([expr, sub_expr]) + for sub_expr in lookups_for_transform(sub_transform) ] else: diff --git a/tests/perf/filters.py b/tests/perf/filters.py index bfa7831..6f8a323 100644 --- a/tests/perf/filters.py +++ b/tests/perf/filters.py @@ -1,4 +1,3 @@ - from django_filters import FilterSet as DFFilterSet from rest_framework_filters import filters @@ -12,17 +11,23 @@ class NoteFilterWithExplicitRelated(DFFilterSet): class Meta: model = Note fields = { - 'title': [ - 'exact', 'contains', 'startswith', 'endswith', - 'iexact', 'icontains', 'istartswith', 'iendswith', + "title": [ + "exact", + "contains", + "startswith", + "endswith", + "iexact", + "icontains", + "istartswith", + "iendswith", ], - 'author__username': ['exact'], + "author__username": ["exact"], } # drf-filters class UserFilterWithAll(DRFFilterSet): - username = filters.AutoFilter(lookups='__all__') + username = filters.AutoFilter(lookups="__all__") class Meta: model = User @@ -30,7 +35,7 @@ class Meta: class NoteFilterWithRelatedAll(DRFFilterSet): - title = filters.AutoFilter(lookups='__all__') + title = filters.AutoFilter(lookups="__all__") author = filters.RelatedFilter(UserFilterWithAll, queryset=User.objects.all()) class Meta: diff --git a/tests/perf/serializers.py b/tests/perf/serializers.py index c6fa770..82b48c2 100644 --- a/tests/perf/serializers.py +++ b/tests/perf/serializers.py @@ -1,4 +1,3 @@ - from rest_framework import serializers from ..testapp.models import Note @@ -7,4 +6,4 @@ class NoteSerializer(serializers.ModelSerializer): class Meta: model = Note - fields = ['pk', 'title', 'content', 'author'] + fields = ["pk", "title", "content", "author"] diff --git a/tests/perf/tests.py b/tests/perf/tests.py index c33fcce..8d8cd70 100644 --- a/tests/perf/tests.py +++ b/tests/perf/tests.py @@ -13,14 +13,19 @@ # parse command verbosity level & use for results output parser = argparse.ArgumentParser() parser.add_argument( - '-v', '--verbosity', action='store', dest='verbosity', default=1, - type=int, choices=[0, 1, 2, 3], + "-v", + "--verbosity", + action="store", + dest="verbosity", + default=1, + type=int, + choices=[0, 1, 2, 3], ) args, _ = parser.parse_known_args() verbosity = args.verbosity -@tag('perf') +@tag("perf") class PerfTestMixin(object): # This mixin provides common setup for testing the performance differences between # django-filter and django-rest-framework-filters. A callable for each implementation @@ -32,13 +37,13 @@ class PerfTestMixin(object): @classmethod def setUpTestData(cls): - bob = models.User.objects.create(username='bob') - joe = models.User.objects.create(username='joe') + bob = models.User.objects.create(username="bob") + joe = models.User.objects.create(username="joe") - models.Note.objects.create(author=bob, title='Note 1') - models.Note.objects.create(author=bob, title='Note 2') - models.Note.objects.create(author=joe, title='Note 3') - models.Note.objects.create(author=joe, title='Note 4') + models.Note.objects.create(author=bob, title="Note 1") + models.Note.objects.create(author=bob, title="Note 2") + models.Note.objects.create(author=joe, title="Note 3") + models.Note.objects.create(author=joe, title="Note 4") def get_callable(self, *args): # Returns the callable and callable's *args to be used for each test @@ -73,28 +78,32 @@ def test_sanity(self): def test_performance(self): call, args = self.get_callable(*self.django_filter_args()) - df_time = min(repeat( - lambda: call(*args), - number=self.iterations, - repeat=self.repeat, - )) + df_time = min( + repeat( + lambda: call(*args), + number=self.iterations, + repeat=self.repeat, + ) + ) call, args = self.get_callable(*self.rest_framework_filters_args()) - drf_time = min(repeat( - lambda: call(*args), - number=self.iterations, - repeat=self.repeat, - )) + drf_time = min( + repeat( + lambda: call(*args), + number=self.iterations, + repeat=self.repeat, + ) + ) diff = (drf_time - df_time) / df_time * 100.0 if verbosity >= 2: - print('\n' + '-' * 32) - print('%s performance' % self.label) - print('django-filter time:\t%.4fs' % df_time) - print('drf-filters time:\t%.4fs' % drf_time) - print('performance diff:\t%+.2f%% ' % diff) - print('-' * 32) + print("\n" + "-" * 32) + print("%s performance" % self.label) + print("django-filter time:\t%.4fs" % df_time) + print("drf-filters time:\t%.4fs" % drf_time) + print("performance diff:\t%+.2f%% " % diff) + print("-" * 32) self.assertLess(drf_time, df_time * self.threshold) @@ -102,18 +111,20 @@ def test_performance(self): class FilterBackendTests(PerfTestMixin, TestCase): # How much faster or slower is drf-filters than django-filter? threshold = 1.5 - label = 'Filter Backend' + label = "Filter Backend" def get_callable(self, view_class): - view = view_class(action_map={'get': 'list'}) - data = {'author__username': 'bob', 'title__contains': 'Note'} - request = factory.get('/', data=data) + view = view_class(action_map={"get": "list"}) + data = {"author__username": "bob", "title__contains": "Note"} + request = factory.get("/", data=data) request = view.initialize_request(request) backend = view.filter_backends[0] call = backend().filter_queryset args = [ - request, view.get_queryset(), view, + request, + view.get_queryset(), + view, ] return call, args @@ -128,27 +139,28 @@ def validate_result(self, qs): self.assertEqual(qs.count(), 2) -@override_settings(ROOT_URLCONF='tests.perf.urls') +@override_settings(ROOT_URLCONF="tests.perf.urls") class WSGIResponseTests(PerfTestMixin, TestCase): # How much does drf-filters affect the request/response cycle? This includes # response rendering, which provides a more practical picture of the performance # costs of using drf-filters. threshold = 1.3 - label = 'WSGI Response' + label = "WSGI Response" def get_callable(self, url): call = self.client.get args = [ - url, {'author__username': 'bob', 'title__contains': 'Note'}, + url, + {"author__username": "bob", "title__contains": "Note"}, ] return call, args def django_filter_args(self): - return ['/df-notes/'] + return ["/df-notes/"] def rest_framework_filters_args(self): - return ['/drf-notes/'] + return ["/drf-notes/"] def validate_result(self, response): self.assertEqual(response.status_code, 200, response.content) diff --git a/tests/perf/urls.py b/tests/perf/urls.py index 8b5da66..829ad5b 100644 --- a/tests/perf/urls.py +++ b/tests/perf/urls.py @@ -1,14 +1,13 @@ - from django.urls import include, path from rest_framework import routers from . import views router = routers.DefaultRouter() -router.register(r'df-notes', views.DFNoteViewSet, basename='df-notes') -router.register(r'drf-notes', views.DRFFNoteViewSet, basename='drf-notes') +router.register(r"df-notes", views.DFNoteViewSet, basename="df-notes") +router.register(r"drf-notes", views.DRFFNoteViewSet, basename="drf-notes") urlpatterns = [ - path('', include(router.urls)), + path("", include(router.urls)), ] diff --git a/tests/related/data.py b/tests/related/data.py index 6103d2e..056268e 100644 --- a/tests/related/data.py +++ b/tests/related/data.py @@ -24,82 +24,82 @@ class RelationshipData: @classmethod def setUpTestData(cls): - b = Blog.objects.create(pk=1, name='Blog A') + b = Blog.objects.create(pk=1, name="Blog A") cls.postA(b) - b = Blog.objects.create(pk=2, name='Blog B') + b = Blog.objects.create(pk=2, name="Blog B") cls.postB(b) - b = Blog.objects.create(pk=3, name='Blog C') + b = Blog.objects.create(pk=3, name="Blog C") cls.postC(b) - b = Blog.objects.create(pk=4, name='Blog D') + b = Blog.objects.create(pk=4, name="Blog D") cls.postD(b) - b = Blog.objects.create(pk=5, name='Blog AB') + b = Blog.objects.create(pk=5, name="Blog AB") cls.postA(b), cls.postB(b) - b = Blog.objects.create(pk=6, name='Blog AC') + b = Blog.objects.create(pk=6, name="Blog AC") cls.postA(b), cls.postC(b) - b = Blog.objects.create(pk=7, name='Blog AD') + b = Blog.objects.create(pk=7, name="Blog AD") cls.postA(b), cls.postD(b) - b = Blog.objects.create(pk=8, name='Blog BC') + b = Blog.objects.create(pk=8, name="Blog BC") cls.postB(b), cls.postC(b) - b = Blog.objects.create(pk=9, name='Blog BD') + b = Blog.objects.create(pk=9, name="Blog BD") cls.postB(b), cls.postD(b) - b = Blog.objects.create(pk=10, name='Blog CD') + b = Blog.objects.create(pk=10, name="Blog CD") cls.postC(b), cls.postD(b) - b = Blog.objects.create(pk=11, name='Blog ABC') + b = Blog.objects.create(pk=11, name="Blog ABC") cls.postA(b), cls.postB(b), cls.postC(b) - b = Blog.objects.create(pk=12, name='Blog ABD') + b = Blog.objects.create(pk=12, name="Blog ABD") cls.postA(b), cls.postB(b), cls.postD(b) - b = Blog.objects.create(pk=13, name='Blog ACD') + b = Blog.objects.create(pk=13, name="Blog ACD") cls.postA(b), cls.postC(b), cls.postD(b) - b = Blog.objects.create(pk=14, name='Blog BCD') + b = Blog.objects.create(pk=14, name="Blog BCD") cls.postB(b), cls.postC(b), cls.postD(b) - b = Blog.objects.create(pk=15, name='Blog ABCD') + b = Blog.objects.create(pk=15, name="Blog ABCD") cls.postA(b), cls.postB(b), cls.postC(b), cls.postD(b) @classmethod def postA(cls, blog): Post.objects.create( blog=blog, - title='Something about Lennon', - publish_date='2008-01-01', + title="Something about Lennon", + publish_date="2008-01-01", ) @classmethod def postB(cls, blog): Post.objects.create( blog=blog, - title='Something about Lennon', - publish_date='2010-01-01', + title="Something about Lennon", + publish_date="2010-01-01", ) @classmethod def postC(cls, blog): Post.objects.create( blog=blog, - title='Ringo was a Starr', - publish_date='2008-01-01', + title="Ringo was a Starr", + publish_date="2008-01-01", ) @classmethod def postD(cls, blog): Post.objects.create( blog=blog, - title='Ringo was a Starr', - publish_date='2010-01-01', + title="Ringo was a Starr", + publish_date="2010-01-01", ) def verify(self, qs, expected): - self.assertQuerySetEqual(qs, expected, attrgetter('pk'), False) + self.assertQuerySetEqual(qs, expected, attrgetter("pk"), False) diff --git a/tests/related/test_exclude.py b/tests/related/test_exclude.py index 3c22beb..3f261c2 100644 --- a/tests/related/test_exclude.py +++ b/tests/related/test_exclude.py @@ -41,19 +41,23 @@ def test_single_exclude(self): # Verify that exclusion is not equivalent # q1 should be equivalent to q2/q4 and *not* q3/q5 - q1 = Blog.objects.exclude(post__title__contains='Lennon') + q1 = Blog.objects.exclude(post__title__contains="Lennon") # nested join - q2 = Blog.objects.exclude(post__in=Post.objects.filter(title__contains='Lennon')) - q3 = Blog.objects.filter(post__in=Post.objects.exclude(title__contains='Lennon')) + q2 = Blog.objects.exclude( + post__in=Post.objects.filter(title__contains="Lennon") + ) + q3 = Blog.objects.filter( + post__in=Post.objects.exclude(title__contains="Lennon") + ) # nested subquery - q4 = Blog.objects.exclude(pk__in=Post.objects - .filter(title__contains='Lennon') - .values('blog')) - q5 = Blog.objects.filter(pk__in=Post.objects - .exclude(title__contains='Lennon') - .values('blog')) + q4 = Blog.objects.exclude( + pk__in=Post.objects.filter(title__contains="Lennon").values("blog") + ) + q5 = Blog.objects.filter( + pk__in=Post.objects.exclude(title__contains="Lennon").values("blog") + ) # C, D, CD *all* entries do not have a title containing 'Lennon' self.verify(q1, [3, 4, 10]) @@ -65,46 +69,44 @@ def test_single_exclude(self): self.verify(q5.distinct(), [3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) def test_chained_join_statements(self): - q1 = Blog.objects \ - .exclude(post__title__contains='Lennon') \ - .exclude(post__publish_date__year=2008) + q1 = Blog.objects.exclude(post__title__contains="Lennon").exclude( + post__publish_date__year=2008 + ) self.verify(q1, self.NOT_CORRECT_ANY) def test_nested_join_outer_exclude(self): q2 = Blog.objects.exclude( - post__in=Post.objects - .filter(title__contains='Lennon') - .filter(publish_date__year=2008), + post__in=Post.objects.filter(title__contains="Lennon").filter( + publish_date__year=2008 + ), ) self.verify(q2, self.CORRECT) def test_nested_join_inner_exclude(self): q3 = Blog.objects.filter( - post__in=Post.objects - .exclude(title__contains='Lennon') - .exclude(publish_date__year=2008), + post__in=Post.objects.exclude(title__contains="Lennon").exclude( + publish_date__year=2008 + ), ) self.verify(q3, self.NOT_CORRECT_ONE) def test_nested_subquery_outer_exclude(self): q4 = Blog.objects.exclude( - pk__in=Post.objects - .filter(title__contains='Lennon') - .filter(publish_date__year=2008) - .values('blog'), + pk__in=Post.objects.filter(title__contains="Lennon") + .filter(publish_date__year=2008) + .values("blog"), ) self.verify(q4, self.CORRECT) def test_nested_subquery_inner_exclude(self): q5 = Blog.objects.filter( - pk__in=Post.objects - .exclude(title__contains='Lennon') - .exclude(publish_date__year=2008) - .values('blog'), + pk__in=Post.objects.exclude(title__contains="Lennon") + .exclude(publish_date__year=2008) + .values("blog"), ) self.verify(q5, self.NOT_CORRECT_ONE) @@ -112,7 +114,7 @@ def test_nested_subquery_inner_exclude(self): # Test behavior def test_reverse_fk(self): GET = { - 'post__title__contains!': 'Lennon', - 'post__publish_date__year!': '2008', + "post__title__contains!": "Lennon", + "post__publish_date__year!": "2008", } self.verify(BlogFilter(GET).qs, self.NOT_CORRECT_ONE) diff --git a/tests/related/test_filter.py b/tests/related/test_filter.py index a48e2a0..cf6607d 100644 --- a/tests/related/test_filter.py +++ b/tests/related/test_filter.py @@ -36,12 +36,11 @@ class FilterTests(RelationshipData, TestCase): def test_single_filter(self): # Verify that the following queries are equivalent - q1 = Blog.objects.filter(post__title__contains='Lennon') - q2 = Blog.objects.filter(post__in=Post.objects - .filter(title__contains='Lennon')) - q3 = Blog.objects.filter(pk__in=Post.objects - .filter(title__contains='Lennon') - .values('blog')) + q1 = Blog.objects.filter(post__title__contains="Lennon") + q2 = Blog.objects.filter(post__in=Post.objects.filter(title__contains="Lennon")) + q3 = Blog.objects.filter( + pk__in=Post.objects.filter(title__contains="Lennon").values("blog") + ) expected = [1, 2, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15] @@ -50,27 +49,26 @@ def test_single_filter(self): self.verify(q3, expected) def test_chained_join_statements(self): - q1 = Blog.objects \ - .filter(post__title__contains='Lennon') \ - .filter(post__publish_date__year=2008) + q1 = Blog.objects.filter(post__title__contains="Lennon").filter( + post__publish_date__year=2008 + ) self.verify(q1.distinct(), self.NOT_CORRECT) def test_nested_join(self): q2 = Blog.objects.filter( - post__in=Post.objects - .filter(title__contains='Lennon') - .filter(publish_date__year=2008), + post__in=Post.objects.filter(title__contains="Lennon").filter( + publish_date__year=2008 + ), ) self.verify(q2, self.CORRECT) def test_nested_subquery(self): q3 = Blog.objects.filter( - pk__in=Post.objects - .filter(title__contains='Lennon') - .filter(publish_date__year=2008) - .values('blog'), + pk__in=Post.objects.filter(title__contains="Lennon") + .filter(publish_date__year=2008) + .values("blog"), ) self.verify(q3, self.CORRECT) @@ -78,7 +76,7 @@ def test_nested_subquery(self): # Test behavior def test_reverse_fk(self): GET = { - 'post__title__contains': 'Lennon', - 'post__publish_date__year': '2008', + "post__title__contains": "Lennon", + "post__publish_date__year": "2008", } self.verify(BlogFilter(GET).qs, self.CORRECT) diff --git a/tests/settings.py b/tests/settings.py index 48fa3ef..209ccb6 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,13 +1,11 @@ - DEBUG = True DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'db.sqlite3', - - 'TEST': { - 'NAME': ':memory:', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "db.sqlite3", + "TEST": { + "NAME": ":memory:", }, }, } @@ -15,37 +13,37 @@ MIDDLEWARE = [] INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.staticfiles', - 'rest_framework_filters', - 'rest_framework', - 'django_filters', - 'tests.testapp', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.staticfiles", + "rest_framework_filters", + "rest_framework", + "django_filters", + "tests.testapp", ) -SECRET_KEY = 'testsecretkey' # noqa +SECRET_KEY = "testsecretkey" # noqa TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], - 'debug': True, + "debug": True, }, }, ] -ROOT_URLCONF = 'tests.testapp.urls' +ROOT_URLCONF = "tests.testapp.urls" -STATIC_URL = '/static/' +STATIC_URL = "/static/" -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/tests/test_backends.py b/tests/test_backends.py index 672a4f8..8379210 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -17,7 +17,7 @@ class RenderMixin: def render(self, viewset_class, data=None): - url = '/' if not data else '/?' + urlencode(data, True) + url = "/" if not data else "/?" + urlencode(data, True) view = viewset_class(action_map={}) backend = view.filter_backends[0] request = view.initialize_request(factory.get(url)) @@ -32,9 +32,9 @@ def setUpTestData(cls): models.User.objects.create(username="user2", email="user2@example.org") def test_django_filter_compatibility(self): - response = self.client.get('/df-users/', {'username': 'user1'}) + response = self.client.get("/df-users/", {"username": "user1"}) self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['username'], 'user1') + self.assertEqual(response.data[0]["username"], "user1") def test_filterset_fields_reusability(self): # Ensure auto-generated FilterSet is reusable w/ filterset_fields. See: @@ -43,23 +43,23 @@ def test_filterset_fields_reusability(self): # Ensure that the filterset_fields aren't altered self.assertDictEqual( views.FilterFieldsUserViewSet.filterset_fields, - {'username': '__all__'}, + {"username": "__all__"}, ) - response = self.client.get('/ff-users/', {'username': 'user1'}) + response = self.client.get("/ff-users/", {"username": "user1"}) self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['username'], 'user1') + self.assertEqual(response.data[0]["username"], "user1") self.assertDictEqual( views.FilterFieldsUserViewSet.filterset_fields, - {'username': '__all__'}, + {"username": "__all__"}, ) - response = self.client.get('/ff-users/', {'username': 'user1'}) + response = self.client.get("/ff-users/", {"username": "user1"}) self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['username'], 'user1') + self.assertEqual(response.data[0]["username"], "user1") self.assertDictEqual( views.FilterFieldsUserViewSet.filterset_fields, - {'username': '__all__'}, + {"username": "__all__"}, ) def test_request_obj_is_passed(test): @@ -77,14 +77,14 @@ def __init__(self, *args, **kwargs): class Meta: model = models.User - fields = ['username'] + fields = ["username"] class ViewSet(views.FilterFieldsUserViewSet): filterset_class = RequestCheck view = ViewSet(action_map={}) backend = view.filter_backends[0] - request = view.initialize_request(factory.get('/')) + request = view.initialize_request(factory.get("/")) backend().filter_queryset(request, view.get_queryset(), view) test.assertTrue(called) @@ -92,18 +92,18 @@ def test_exclusion(self): class RequestCheck(FilterSet): class Meta: model = models.User - fields = ['username'] + fields = ["username"] class ViewSet(views.FilterFieldsUserViewSet): filterset_class = RequestCheck view = ViewSet(action_map={}) backend = view.filter_backends[0] - request = view.initialize_request(factory.get('/?username=user1')) + request = view.initialize_request(factory.get("/?username=user1")) qs = backend().filter_queryset(request, view.get_queryset(), view) self.assertEqual([u.pk for u in qs], [1]) - request = view.initialize_request(factory.get('/?username!=user1')) + request = view.initialize_request(factory.get("/?username!=user1")) qs = backend().filter_queryset(request, view.get_queryset(), view) self.assertEqual([u.pk for u in qs], [2]) @@ -113,12 +113,12 @@ def test_disabled(self): # see: https://github.com/philipn/django-rest-framework-filters/issues/230 view = views.UnfilteredUserViewSet(action_map={}) backend = view.filter_backends[0] - request = view.initialize_request(factory.get('/')) + request = view.initialize_request(factory.get("/")) # ensure view has backend and is missing attributes self.assertIs(backend, RestFrameworkFilterBackend) - self.assertFalse(hasattr(view, 'filterset_class')) - self.assertFalse(hasattr(view, 'filterset_fields')) + self.assertFalse(hasattr(view, "filterset_class")) + self.assertFalse(hasattr(view, "filterset_fields")) # filterset should be None, method should not error self.assertIsNone(backend().get_filterset(request, view.queryset, view)) @@ -134,9 +134,11 @@ class BackendRenderingTests(RenderMixin, APITestCase): def test_sanity(self): # Sanity check to ensure backend can render without crashing. class SimpleViewSet(views.FilterFieldsUserViewSet): - filterset_fields = ['username'] + filterset_fields = ["username"] - self.assertHTMLEqual(self.render(SimpleViewSet), """ + self.assertHTMLEqual( + self.render(SimpleViewSet), + """

Field filters

@@ -145,18 +147,21 @@ class SimpleViewSet(views.FilterFieldsUserViewSet):

- """) + """, + ) def test_django_filter_filterset_compatibility(self): class SimpleFilterSet(django_filters.FilterSet): class Meta: model = models.User - fields = ['username'] + fields = ["username"] class SimpleViewSet(views.FilterFieldsUserViewSet): filterset_class = SimpleFilterSet - self.assertHTMLEqual(self.render(SimpleViewSet), """ + self.assertHTMLEqual( + self.render(SimpleViewSet), + """

Field filters

@@ -165,7 +170,8 @@ class SimpleViewSet(views.FilterFieldsUserViewSet):

- """) + """, + ) def test_related_filterset(self): class UserFilter(FilterSet): @@ -175,13 +181,15 @@ class NoteFilter(FilterSet): author = filters.RelatedFilter( filterset=UserFilter, queryset=models.User.objects.all(), - label='Writer', + label="Writer", ) class RelatedViewSet(views.NoteViewSet): filterset_class = NoteFilter - self.assertHTMLEqual(self.render(RelatedViewSet), """ + self.assertHTMLEqual( + self.render(RelatedViewSet), + """

Field filters

@@ -202,7 +210,8 @@ class RelatedViewSet(views.NoteViewSet):

- """) + """, + ) def test_related_filterset_validation(self): class UserFilter(FilterSet): @@ -212,14 +221,16 @@ class NoteFilter(FilterSet): author = filters.RelatedFilter( filterset=UserFilter, queryset=models.User.objects.all(), - label='Writer', + label="Writer", ) class RelatedViewSet(views.NoteViewSet): filterset_class = NoteFilter - context = {'author': 'invalid', 'author__last_login': 'invalid'} - self.assertHTMLEqual(self.render(RelatedViewSet, context), """ + context = {"author": "invalid", "author__last_login": "invalid"} + self.assertHTMLEqual( + self.render(RelatedViewSet, context), + """

Field filters

    @@ -253,7 +264,8 @@ class RelatedViewSet(views.NoteViewSet): - """) + """, + ) def test_rendering_doesnt_affect_filterset_classes(self): class NoteFilter(FilterSet): @@ -261,7 +273,7 @@ class NoteFilter(FilterSet): class UserFilter(FilterSet): notes = filters.RelatedFilter( - field_name='note', + field_name="note", filterset=NoteFilter, queryset=models.Note.objects.all(), ) @@ -277,7 +289,7 @@ class SimpleViewSet(views.FilterFieldsUserViewSet): self.assertFalse(issubclass(filterset, SubsetDisabledMixin)) # check that FilterSet.related_filters aren't modified - filterset = UserFilter.base_filters['notes'].filterset + filterset = UserFilter.base_filters["notes"].filterset self.assertTrue(issubclass(filterset, FilterSet)) self.assertFalse(issubclass(filterset, SubsetDisabledMixin)) @@ -287,7 +299,7 @@ class NoteFilter(FilterSet): class UserFilter(FilterSet): notes = filters.RelatedFilter( - field_name='note', + field_name="note", filterset=NoteFilter, queryset=models.Note.objects.all(), ) @@ -296,7 +308,7 @@ class SimpleViewSet(views.FilterClassUserViewSet): filterset_class = UserFilter view = SimpleViewSet(action_map={}) - request = view.initialize_request(factory.get('/')) + request = view.initialize_request(factory.get("/")) backend = view.filter_backends[0] backend = backend() @@ -309,7 +321,7 @@ class SimpleViewSet(views.FilterClassUserViewSet): self.assertIsInstance(filterset, SubsetDisabledMixin) # check related filtersets - filterset = filterset.related_filtersets['notes'] + filterset = filterset.related_filtersets["notes"] self.assertIsInstance(filterset, FilterSet) self.assertIsInstance(filterset, SubsetDisabledMixin) @@ -318,7 +330,7 @@ class SimpleViewSet(views.FilterClassUserViewSet): def test_patch_for_rendering_handles_exception(self): view = views.FilterClassUserViewSet(action_map={}) - request = view.initialize_request(factory.get('/')) + request = view.initialize_request(factory.get("/")) backend = view.filter_backends[0] backend = backend() @@ -341,69 +353,75 @@ def setUpTestData(cls): models.User.objects.create(username="user4", email="user4@example.org") def test_valid(self): - readable = quote('(username%3Duser1)|(email__contains%3Dexample.org)') - response = self.client.get('/ffcomplex-users/?filters=' + readable) + readable = quote("(username%3Duser1)|(email__contains%3Dexample.org)") + response = self.client.get("/ffcomplex-users/?filters=" + readable) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertListEqual( - [r['username'] for r in response.data], - ['user1', 'user3', 'user4'], + [r["username"] for r in response.data], + ["user1", "user3", "user4"], ) def test_invalid(self): - readable = quote('(username%3Duser1)asdf(email__contains%3Dexample.org)') - response = self.client.get('/ffcomplex-users/?filters=' + readable) + readable = quote("(username%3Duser1)asdf(email__contains%3Dexample.org)") + response = self.client.get("/ffcomplex-users/?filters=" + readable) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertDictEqual(response.data, { - 'filters': ["Invalid querystring operator. Matched: 'asdf'."], - }) + self.assertDictEqual( + response.data, + { + "filters": ["Invalid querystring operator. Matched: 'asdf'."], + }, + ) def test_invalid_filterset_errors(self): - readable = quote('(id%3Dfoo) | (id%3Dbar)') - response = self.client.get('/ffcomplex-users/?filters=' + readable) + readable = quote("(id%3Dfoo) | (id%3Dbar)") + response = self.client.get("/ffcomplex-users/?filters=" + readable) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertDictEqual(response.data, { - 'filters': { - 'id=foo': { - 'id': ['Enter a number.'], - }, - 'id=bar': { - 'id': ['Enter a number.'], + self.assertDictEqual( + response.data, + { + "filters": { + "id=foo": { + "id": ["Enter a number."], + }, + "id=bar": { + "id": ["Enter a number."], + }, }, }, - }) + ) def test_pagination_compatibility(self): # Ensure that complex-filtering does not affect additional query param processing. - readable = quote('(email__contains%3Dexample.org)') + readable = quote("(email__contains%3Dexample.org)") # sanity check w/o pagination - response = self.client.get('/ffcomplex-users/?filters=' + readable) + response = self.client.get("/ffcomplex-users/?filters=" + readable) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertListEqual( - [r['username'] for r in response.data], - ['user3', 'user4'], + [r["username"] for r in response.data], + ["user3", "user4"], ) # sanity check w/o complex-filtering - response = self.client.get('/ffcomplex-users/?page_size=1') + response = self.client.get("/ffcomplex-users/?page_size=1") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn('results', response.data) + self.assertIn("results", response.data) self.assertListEqual( - [r['username'] for r in response.data['results']], - ['user1'], + [r["username"] for r in response.data["results"]], + ["user1"], ) # pagination + complex-filtering - response = self.client.get('/ffcomplex-users/?page_size=1&filters=' + readable) + response = self.client.get("/ffcomplex-users/?page_size=1&filters=" + readable) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn('results', response.data) + self.assertIn("results", response.data) self.assertListEqual( - [r['username'] for r in response.data['results']], - ['user3'], + [r["username"] for r in response.data["results"]], + ["user3"], ) diff --git a/tests/test_complex_ops.py b/tests/test_complex_ops.py index 20f7466..a5f5013 100644 --- a/tests/test_complex_ops.py +++ b/tests/test_complex_ops.py @@ -6,9 +6,9 @@ from django.test import TestCase from rest_framework.serializers import ValidationError -from rest_framework_filters.complex_ops import ( - ComplexOp, combine_complex_queryset, decode_complex_ops, -) +from rest_framework_filters.complex_ops import (ComplexOp, + combine_complex_queryset, + decode_complex_ops) from tests.testapp import models @@ -18,7 +18,7 @@ def encode(querysting): # Python 3.7 added '~' to the reserved character set. if sys.version_info < (3, 7): - result = result.replace('%7E', '~') + result = result.replace("%7E", "~") return result @@ -26,144 +26,159 @@ def encode(querysting): class DecodeComplexOpsTests(TestCase): def test_docstring(self): - encoded = '%28a%253D1%29%20%26%20%28b%253D2%29%20%7C%20~%28c%253D3%29' - readable = '(a%3D1) & (b%3D2) | ~(c%3D3)' + encoded = "%28a%253D1%29%20%26%20%28b%253D2%29%20%7C%20~%28c%253D3%29" + readable = "(a%3D1) & (b%3D2) | ~(c%3D3)" result = [ - ('a=1', False, QuerySet.__and__), - ('b=2', False, QuerySet.__or__), - ('c=3', True, None), + ("a=1", False, QuerySet.__and__), + ("b=2", False, QuerySet.__or__), + ("c=3", True, None), ] self.assertEqual(encode(readable), encoded) self.assertEqual(decode_complex_ops(encoded), result) def test_single_op(self): - encoded = '%28a%253D1%29' - readable = '(a%3D1)' + encoded = "%28a%253D1%29" + readable = "(a%3D1)" result = [ - ('a=1', False, None), + ("a=1", False, None), ] self.assertEqual(encode(readable), encoded) self.assertEqual(decode_complex_ops(encoded), result) def test_op_spacing(self): - encoded = '%28a%253D1%29%20%26%20%28b%253D2%29' - readable = '(a%3D1) & (b%3D2)' + encoded = "%28a%253D1%29%20%26%20%28b%253D2%29" + readable = "(a%3D1) & (b%3D2)" result = [ - ('a=1', False, QuerySet.__and__), - ('b=2', False, None), + ("a=1", False, QuerySet.__and__), + ("b=2", False, None), ] self.assertEqual(encode(readable), encoded) self.assertEqual(decode_complex_ops(encoded), result) def test_missing_parens(self): - encoded = 'a%253D1' - readable = 'a%3D1' + encoded = "a%253D1" + readable = "a%3D1" self.assertEqual(encode(readable), encoded) with self.assertRaises(ValidationError) as exc: decode_complex_ops(encoded) - self.assertEqual(exc.exception.detail, [ - "Unable to parse querystring. Decoded: 'a%3D1'.", - ]) + self.assertEqual( + exc.exception.detail, + [ + "Unable to parse querystring. Decoded: 'a%3D1'.", + ], + ) def test_missing_closing_paren(self): - encoded = '%28a%253D1' - readable = '(a%3D1' + encoded = "%28a%253D1" + readable = "(a%3D1" self.assertEqual(encode(readable), encoded) with self.assertRaises(ValidationError) as exc: decode_complex_ops(encoded) - self.assertEqual(exc.exception.detail, [ - "Unable to parse querystring. Decoded: '(a%3D1'.", - ]) + self.assertEqual( + exc.exception.detail, + [ + "Unable to parse querystring. Decoded: '(a%3D1'.", + ], + ) def test_missing_op(self): - encoded = '%28a%253D1%29%28b%253D2%29%26%28c%253D3%29' - readable = '(a%3D1)(b%3D2)&(c%3D3)' + encoded = "%28a%253D1%29%28b%253D2%29%26%28c%253D3%29" + readable = "(a%3D1)(b%3D2)&(c%3D3)" self.assertEqual(encode(readable), encoded) with self.assertRaises(ValidationError) as exc: decode_complex_ops(encoded) - self.assertEqual(exc.exception.detail, [ - "Invalid querystring operator. Matched: ''.", - ]) + self.assertEqual( + exc.exception.detail, + [ + "Invalid querystring operator. Matched: ''.", + ], + ) def test_invalid_ops(self): - encoded = '%28a%253D1%29asdf%28b%253D2%29qwerty%28c%253D3%29%26' - readable = '(a%3D1)asdf(b%3D2)qwerty(c%3D3)&' + encoded = "%28a%253D1%29asdf%28b%253D2%29qwerty%28c%253D3%29%26" + readable = "(a%3D1)asdf(b%3D2)qwerty(c%3D3)&" self.assertEqual(encode(readable), encoded) with self.assertRaises(ValidationError) as exc: decode_complex_ops(encoded) - self.assertEqual(exc.exception.detail, [ - "Invalid querystring operator. Matched: 'asdf'.", - "Invalid querystring operator. Matched: 'qwerty'.", - "Ending querystring must not have trailing characters. Matched: '&'.", - ]) + self.assertEqual( + exc.exception.detail, + [ + "Invalid querystring operator. Matched: 'asdf'.", + "Invalid querystring operator. Matched: 'qwerty'.", + "Ending querystring must not have trailing characters. Matched: '&'.", + ], + ) def test_negation(self): - encoded = '%28a%253D1%29%20%26%20~%28b%253D2%29' - readable = '(a%3D1) & ~(b%3D2)' + encoded = "%28a%253D1%29%20%26%20~%28b%253D2%29" + readable = "(a%3D1) & ~(b%3D2)" result = [ - ('a=1', False, QuerySet.__and__), - ('b=2', True, None), + ("a=1", False, QuerySet.__and__), + ("b=2", True, None), ] self.assertEqual(encode(readable), encoded) self.assertEqual(decode_complex_ops(encoded), result) def test_leading_negation(self): - encoded = '~%28a%253D1%29%20%26%20%28b%253D2%29' - readable = '~(a%3D1) & (b%3D2)' + encoded = "~%28a%253D1%29%20%26%20%28b%253D2%29" + readable = "~(a%3D1) & (b%3D2)" result = [ - ('a=1', True, QuerySet.__and__), - ('b=2', False, None), + ("a=1", True, QuerySet.__and__), + ("b=2", False, None), ] self.assertEqual(encode(readable), encoded) self.assertEqual(decode_complex_ops(encoded), result) def test_only_negation(self): - encoded = '~%28a%253D1%29' - readable = '~(a%3D1)' + encoded = "~%28a%253D1%29" + readable = "~(a%3D1)" result = [ - ('a=1', True, None), + ("a=1", True, None), ] self.assertEqual(encode(readable), encoded) self.assertEqual(decode_complex_ops(encoded), result) def test_duplicate_negation(self): - encoded = '%28a%253D1%29%20%26%20~~%28b%253D2%29' - readable = '(a%3D1) & ~~(b%3D2)' + encoded = "%28a%253D1%29%20%26%20~~%28b%253D2%29" + readable = "(a%3D1) & ~~(b%3D2)" self.assertEqual(encode(readable), encoded) with self.assertRaises(ValidationError) as exc: decode_complex_ops(encoded) - self.assertEqual(exc.exception.detail, [ - "Invalid querystring operator. Matched: ' & ~'.", - ]) + self.assertEqual( + exc.exception.detail, + [ + "Invalid querystring operator. Matched: ' & ~'.", + ], + ) def test_tilde_decoding(self): # Ensure decoding handles both RFC 2396 & 3986 - encoded_rfc3986 = '~%28a%253D1%29' - encoded_rfc2396 = '%7E%28a%253D1%29' - readable = '~(a%3D1)' + encoded_rfc3986 = "~%28a%253D1%29" + encoded_rfc2396 = "%7E%28a%253D1%29" + readable = "~(a%3D1)" result = [ - ('a=1', True, None), + ("a=1", True, None), ] self.assertEqual(encode(readable), encoded_rfc3986) @@ -175,24 +190,26 @@ class CombineComplexQuerysetTests(TestCase): @classmethod def setUpTestData(cls): - models.User.objects.create(username='u1', first_name='Bob', last_name='Jones') - models.User.objects.create(username='u2', first_name='Joe', last_name='Jones') - models.User.objects.create(username='u3', first_name='Bob', last_name='Smith') - models.User.objects.create(username='u4', first_name='Joe', last_name='Smith') + models.User.objects.create(username="u1", first_name="Bob", last_name="Jones") + models.User.objects.create(username="u2", first_name="Joe", last_name="Jones") + models.User.objects.create(username="u3", first_name="Bob", last_name="Smith") + models.User.objects.create(username="u4", first_name="Joe", last_name="Smith") def test_single(self): - querysets = [models.User.objects.filter(first_name='Bob')] + querysets = [models.User.objects.filter(first_name="Bob")] complex_ops = [ComplexOp(None, False, None)] self.assertQuerySetEqual( combine_complex_queryset(querysets, complex_ops), - ['u1', 'u3'], attrgetter('username'), False, + ["u1", "u3"], + attrgetter("username"), + False, ) def test_AND(self): querysets = [ - models.User.objects.filter(first_name='Bob'), - models.User.objects.filter(last_name='Jones'), + models.User.objects.filter(first_name="Bob"), + models.User.objects.filter(last_name="Jones"), ] complex_ops = [ ComplexOp(None, False, QuerySet.__and__), @@ -201,13 +218,15 @@ def test_AND(self): self.assertQuerySetEqual( combine_complex_queryset(querysets, complex_ops), - ['u1'], attrgetter('username'), False, + ["u1"], + attrgetter("username"), + False, ) def test_OR(self): querysets = [ - models.User.objects.filter(first_name='Bob'), - models.User.objects.filter(last_name='Smith'), + models.User.objects.filter(first_name="Bob"), + models.User.objects.filter(last_name="Smith"), ] complex_ops = [ ComplexOp(None, False, QuerySet.__or__), @@ -216,13 +235,15 @@ def test_OR(self): self.assertQuerySetEqual( combine_complex_queryset(querysets, complex_ops), - ['u1', 'u3', 'u4'], attrgetter('username'), False, + ["u1", "u3", "u4"], + attrgetter("username"), + False, ) def test_negation(self): querysets = [ - models.User.objects.filter(first_name='Bob'), - models.User.objects.filter(last_name='Smith'), + models.User.objects.filter(first_name="Bob"), + models.User.objects.filter(last_name="Smith"), ] complex_ops = [ ComplexOp(None, False, QuerySet.__and__), @@ -231,5 +252,7 @@ def test_negation(self): self.assertQuerySetEqual( combine_complex_queryset(querysets, complex_ops), - ['u1'], attrgetter('username'), False, + ["u1"], + attrgetter("username"), + False, ) diff --git a/tests/test_filtering.py b/tests/test_filtering.py index 888e30c..fb399da 100644 --- a/tests/test_filtering.py +++ b/tests/test_filtering.py @@ -3,13 +3,12 @@ from rest_framework_filters import FilterSet, filters -from .testapp.filters import ( - AccountFilter, CFilter, CoverFilter, CustomerFilter, NoteFilter, NoteFilterWithAlias, - NoteFilterWithRelatedAlias, PageFilter, PersonFilter, PostFilter, UserFilter, -) -from .testapp.models import ( - A, Account, B, C, Cover, Customer, Note, Page, Person, Post, Tag, User, -) +from .testapp.filters import (AccountFilter, CFilter, CoverFilter, + CustomerFilter, NoteFilter, NoteFilterWithAlias, + NoteFilterWithRelatedAlias, PageFilter, + PersonFilter, PostFilter, UserFilter) +from .testapp.models import (A, Account, B, C, Cover, Customer, Note, Page, + Person, Post, Tag, User) class LocalTagFilter(FilterSet): @@ -33,23 +32,27 @@ def setUpTestData(cls): ####################### Note.objects.create(title="Test 1", content="Test content 1", author=user1) Note.objects.create(title="Test 2", content="Test content 2", author=user1) - Note.objects.create(title="Hello Test 3", content="Test content 3", author=user1) - Note.objects.create(title="Hello Test 4", content="Test content 4", author=user2) + Note.objects.create( + title="Hello Test 3", content="Test content 3", author=user1 + ) + Note.objects.create( + title="Hello Test 4", content="Test content 4", author=user2 + ) def test_all_lookups(self): # Test __iendswith - GET = {'title__iendswith': '2'} + GET = {"title__iendswith": "2"} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(list(f.qs)[0].title, "Test 2") # Test __contains - GET = {'title__contains': 'Test'} + GET = {"title__contains": "Test"} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 4) # Test that the default exact filter works - GET = {'title': 'Hello Test 3'} + GET = {"title": "Hello Test 3"} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(list(f.qs)[0].title, "Hello Test 3") @@ -58,7 +61,7 @@ def test_autofilter_with_mixin(self): # Mixin FilterSets should not error when no model is provided. See: # https://github.com/philipn/django-rest-framework-filters/issues/82 class Mixin(FilterSet): - title = filters.AutoFilter(lookups='__all__') + title = filters.AutoFilter(lookups="__all__") class Actual(Mixin): class Meta: @@ -70,11 +73,11 @@ class Meta: model = Note fields = [] - GET = {'title__contains': 'Hello'} + GET = {"title__contains": "Hello"} f = Actual(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 2) - GET = {'title__contains': 'Hello'} + GET = {"title__contains": "Hello"} f = Subclass(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 2) @@ -82,14 +85,16 @@ def test_autofilter_with_method(self): # Test that method param applies to all auto-generated filters. def filter_iexact(qs, field_name, value): # Test that the field name contains the lookup expression. - self.assertEqual(field_name, 'content__icontains') + self.assertEqual(field_name, "content__icontains") return qs.filter(**{field_name: value}, author__username="user1") class Actual(FilterSet): - title = filters.AutoFilter(lookups='__all__', method='filter_title') - content = filters.AutoFilter(lookups=['icontains'], method=filter_iexact) - author = filters.AutoFilter(lookups='__all__', field_name='author__username', method='filter_author') + title = filters.AutoFilter(lookups="__all__", method="filter_title") + content = filters.AutoFilter(lookups=["icontains"], method=filter_iexact) + author = filters.AutoFilter( + lookups="__all__", field_name="author__username", method="filter_author" + ) class Meta: model = Note @@ -102,36 +107,36 @@ def filter_author(self, qs, field_name, value): return qs.filter(**{field_name: value}) # Test method as a function - GET = {'content__icontains': 'test content'} + GET = {"content__icontains": "test content"} f = Actual(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 3) # Test method as a string reference to filterset method - GET = {'title__contains': 'Hello'} + GET = {"title__contains": "Hello"} f = Actual(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(f.qs.first().author.username, "user1") - GET = {'title__iendswith': '4'} + GET = {"title__iendswith": "4"} f = Actual(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 0) - GET = {'title': 'Hello Test 4'} + GET = {"title": "Hello Test 4"} f = Actual(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 0) - GET = {'title': 'Hello Test 3'} + GET = {"title": "Hello Test 3"} f = Actual(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(f.qs.first().author.username, "user1") # Test method in Autofilter on related field - GET = {'author__contains': 'user2'} + GET = {"author__contains": "user2"} f = Actual(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(f.qs.first().author.username, "user2") - GET = {'author': 'user2'} + GET = {"author": "user2"} f = Actual(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(f.qs.first().author.username, "user2") @@ -148,18 +153,18 @@ def setUpTestData(cls): ######################################################################## # Create notes ######################################################### - note1 = Note.objects.create(title="Test 1", - content="Test content 1", - author=user1) - note2 = Note.objects.create(title="Test 2", - content="Test content 2", - author=user1) - Note.objects.create(title="Hello Test 3", - content="Test content 3", - author=user1) - note4 = Note.objects.create(title="Hello Test 4", - content="Test content 4", - author=user2) + note1 = Note.objects.create( + title="Test 1", content="Test content 1", author=user1 + ) + note2 = Note.objects.create( + title="Test 2", content="Test content 2", author=user1 + ) + Note.objects.create( + title="Hello Test 3", content="Test content 3", author=user1 + ) + note4 = Note.objects.create( + title="Hello Test 4", content="Test content 4", author=user2 + ) ######################################################################## # Create posts ######################################################### @@ -174,17 +179,16 @@ def setUpTestData(cls): ######################################################################## # Create pages ######################################################### - Page.objects.create(title="First page", - content="First first.") - Page.objects.create(title="Second page", - content="Second second.", - previous_page_id=1) - Page.objects.create(title="Third page", - content="Third third.", - previous_page_id=2) - Page.objects.create(title="Fourth page", - content="Fourth fourth.", - previous_page_id=3) + Page.objects.create(title="First page", content="First first.") + Page.objects.create( + title="Second page", content="Second second.", previous_page_id=1 + ) + Page.objects.create( + title="Third page", content="Third third.", previous_page_id=2 + ) + Page.objects.create( + title="Fourth page", content="Fourth fourth.", previous_page_id=3 + ) ######################################################################## # ManyToMany ########################################################### @@ -223,24 +227,28 @@ def setUpTestData(cls): ######################################################################## # to_field relations ################################################### - c1 = Customer.objects.create(name='Bob Jones', ssn='111111111', dob='1990-01-01') - c2 = Customer.objects.create(name='Sue Jones', ssn='222222222', dob='1990-01-01') + c1 = Customer.objects.create( + name="Bob Jones", ssn="111111111", dob="1990-01-01" + ) + c2 = Customer.objects.create( + name="Sue Jones", ssn="222222222", dob="1990-01-01" + ) - Account.objects.create(customer=c1, type='c', name='Bank 1 checking') - Account.objects.create(customer=c1, type='s', name='Bank 1 savings') - Account.objects.create(customer=c2, type='c', name='Bank 1 checking 1') - Account.objects.create(customer=c2, type='c', name='Bank 1 checking 2') - Account.objects.create(customer=c2, type='s', name='Bank 2 savings') + Account.objects.create(customer=c1, type="c", name="Bank 1 checking") + Account.objects.create(customer=c1, type="s", name="Bank 1 savings") + Account.objects.create(customer=c2, type="c", name="Bank 1 checking 1") + Account.objects.create(customer=c2, type="c", name="Bank 1 checking 2") + Account.objects.create(customer=c2, type="s", name="Bank 2 savings") def test_relatedfilter(self): # Test that the default exact filter works - GET = {'author': User.objects.get(username='user2').pk} + GET = {"author": User.objects.get(username="user2").pk} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(list(f.qs)[0].title, "Hello Test 4") # Test the username filter on the related UserFilter set. - GET = {'author__username': 'user2'} + GET = {"author__username": "user2"} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(list(f.qs)[0].title, "Hello Test 4") @@ -249,35 +257,35 @@ def test_relatedfilter_for_related_all_lookups(self): # ensure that filters work for AutoFilter across a RelatedFilter. # Test that the default exact filter works - GET = {'author': User.objects.get(username='user2').pk} + GET = {"author": User.objects.get(username="user2").pk} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) note = list(f.qs)[0] self.assertEqual(note.title, "Hello Test 4") # Test the username filter on the related UserFilter set. - GET = {'author__username': 'user2'} + GET = {"author__username": "user2"} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(list(f.qs)[0].title, "Hello Test 4") - GET = {'author__username__endswith': '2'} + GET = {"author__username__endswith": "2"} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(list(f.qs)[0].title, "Hello Test 4") - GET = {'author__username__endswith': '1'} + GET = {"author__username__endswith": "1"} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 3) - GET = {'author__username__contains': 'user'} + GET = {"author__username__contains": "user"} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 4) def test_relatedfilter_for_related_all_lookups_and_different_filter_name(self): # Test that the default exact filter works GET = { - 'writer': User.objects.get(username='user2').pk, + "writer": User.objects.get(username="user2").pk, } f = NoteFilterWithAlias(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -285,31 +293,31 @@ def test_relatedfilter_for_related_all_lookups_and_different_filter_name(self): self.assertEqual(note.title, "Hello Test 4") # Test the username filter on the related UserFilter set. - GET = {'writer__username': 'user2'} + GET = {"writer__username": "user2"} f = NoteFilterWithAlias(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(list(f.qs)[0].title, "Hello Test 4") - GET = {'writer__username__endswith': '2'} + GET = {"writer__username__endswith": "2"} f = NoteFilterWithAlias(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) self.assertEqual(list(f.qs)[0].title, "Hello Test 4") - GET = {'writer__username__endswith': '1'} + GET = {"writer__username__endswith": "1"} f = NoteFilterWithAlias(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 3) - GET = {'writer__username__contains': 'user'} + GET = {"writer__username__contains": "user"} f = NoteFilterWithAlias(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 4) def test_relatedfilter_for_aliased_nested_relationships(self): - qs = Page.objects.order_by('pk') + qs = Page.objects.order_by("pk") - f1 = PageFilter({'two_pages_back': '1'}, queryset=qs) - f2 = PageFilter({'two_pages_back': '2'}, queryset=qs) - f3 = PageFilter({'two_pages_back': '3'}, queryset=qs) - f4 = PageFilter({'two_pages_back': '4'}, queryset=qs) + f1 = PageFilter({"two_pages_back": "1"}, queryset=qs) + f2 = PageFilter({"two_pages_back": "2"}, queryset=qs) + f3 = PageFilter({"two_pages_back": "3"}, queryset=qs) + f4 = PageFilter({"two_pages_back": "4"}, queryset=qs) self.assertQuerySetEqual(f1.qs, [3], lambda p: p.pk) self.assertQuerySetEqual(f2.qs, [4], lambda p: p.pk) @@ -319,7 +327,7 @@ def test_relatedfilter_for_aliased_nested_relationships(self): def test_relatedfilter_different_name(self): # Test the name filter on the related UserFilter set. GET = { - 'author__name': 'user2', + "author__name": "user2", } f = NoteFilterWithRelatedAlias(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -328,7 +336,7 @@ def test_relatedfilter_different_name(self): def test_double_relation_filter(self): GET = { - 'note__author__username__endswith': 'user2', + "note__author__username__endswith": "user2", } f = PostFilter(GET, queryset=Post.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -337,7 +345,7 @@ def test_double_relation_filter(self): def test_triple_relation_filter(self): GET = { - 'post__note__author__username__endswith': 'user2', + "post__note__author__username__endswith": "user2", } f = CoverFilter(GET, queryset=Cover.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -346,7 +354,7 @@ def test_triple_relation_filter(self): def test_indirect_recursive_relation(self): GET = { - 'a__b__name__endswith': '1', + "a__b__name__endswith": "1", } f = CFilter(GET, queryset=C.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -356,7 +364,7 @@ def test_indirect_recursive_relation(self): def test_direct_recursive_relation(self): # see: https://github.com/philipn/django-rest-framework-filters/issues/333 GET = { - 'best_friend': 1, + "best_friend": 1, } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -365,7 +373,7 @@ def test_direct_recursive_relation(self): def test_direct_recursive_relation__lookup(self): GET = { - 'best_friend__name__endswith': 'hn', + "best_friend__name__endswith": "hn", } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -374,7 +382,7 @@ def test_direct_recursive_relation__lookup(self): def test_m2m_relation(self): GET = { - 'tags__name__endswith': 'ark', + "tags__name__endswith": "ark", } f = PostFilter(GET, queryset=Post.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -382,47 +390,47 @@ def test_m2m_relation(self): self.assertEqual(p.content, "Test content in post 1") GET = { - 'tags__name__endswith': 'ouse', + "tags__name__endswith": "ouse", } f = PostFilter(GET, queryset=Post.objects.all()) self.assertEqual(len(list(f.qs)), 2) contents = {post.content for post in f.qs} - self.assertEqual(contents, {'Test content in post 1', 'Test content in post 3'}) + self.assertEqual(contents, {"Test content in post 1", "Test content in post 3"}) def test_m2m_distinct(self): GET = { - 'tags__name__startswith': 'test', + "tags__name__startswith": "test", } f = PostFilter(GET, queryset=Post.objects.all()) self.assertEqual(len(list(f.qs)), 1) contents = {post.content for post in f.qs} - self.assertEqual(contents, {'Test content in post 2'}) + self.assertEqual(contents, {"Test content in post 2"}) def test_nonexistent_related_field(self): # Invalid filter keys (including those on related filters) should be ignored. # Related: https://github.com/philipn/django-rest-framework-filters/issues/58 GET = { - 'author__nonexistent': 'foobar', + "author__nonexistent": "foobar", } f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 4) GET = { - 'author__nonexistent__isnull': 'foobar', + "author__nonexistent__isnull": "foobar", } f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 4) def test_related_filters_inheritance(self): class ChildFilter(PostFilter): - foo = filters.RelatedFilter(NoteFilter, field_name='note') + foo = filters.RelatedFilter(NoteFilter, field_name="note") self.assertEqual( - ['author', 'note', 'tags'], + ["author", "note", "tags"], list(PostFilter.related_filters), ) self.assertEqual( - ['author', 'note', 'tags', 'foo'], + ["author", "note", "tags", "foo"], list(ChildFilter.related_filters), ) @@ -432,16 +440,18 @@ def test_relatedfilter_queryset_required(self): # The default behavior should not expose information, which requires users to # explicitly set the `queryset` argument. class NoteFilter(FilterSet): - title = filters.CharFilter(field_name='title') - author = filters.RelatedFilter(UserFilter, name='author') + title = filters.CharFilter(field_name="title") + author = filters.RelatedFilter(UserFilter, name="author") class Meta: model = Note fields = [] - GET = {'author': User.objects.get(username='user2').pk} - msg = "Expected `.get_queryset()` for related filter 'NoteFilter.author' " \ - "to return a `QuerySet`, but got `None`." + GET = {"author": User.objects.get(username="user2").pk} + msg = ( + "Expected `.get_queryset()` for related filter 'NoteFilter.author' " + "to return a `QuerySet`, but got `None`." + ) with self.assertRaisesMessage(ValueError, msg): NoteFilter(GET, queryset=Note.objects.all()) @@ -458,7 +468,7 @@ def __init__(self, *args, **kwargs): class Meta: model = User - fields = ['username'] + fields = ["username"] class NoteFilter(FilterSet): author = filters.RelatedFilter(RequestCheck, queryset=User.objects.all()) @@ -467,7 +477,7 @@ class Meta: model = Note fields = [] - GET = {'author__username': 'user2'} + GET = {"author__username": "user2"} # should pass NoteFilter(GET, queryset=Note.objects.all(), request=object()).qs @@ -475,11 +485,11 @@ class Meta: def test_validation(self): class F(PostFilter): - pk = filters.NumberFilter(field_name='id') + pk = filters.NumberFilter(field_name="id") GET = { - 'note__author': 'foo', - 'pk': 'bar', + "note__author": "foo", + "pk": "bar", } f = F(GET, queryset=Post.objects.all()) @@ -487,33 +497,33 @@ class F(PostFilter): self.assertFalse(f.is_valid()) self.assertEqual(len(f.form.errors.keys()), 2) - self.assertIn('note__author', f.form.errors) - self.assertIn('pk', f.form.errors) + self.assertIn("note__author", f.form.errors) + self.assertIn("pk", f.form.errors) def test_relative_filterset_path(self): # Test that RelatedFilter can import FilterSets by name from its parent's module class PostFilter(FilterSet): - tags = filters.RelatedFilter('LocalTagFilter', queryset=Tag.objects.all()) + tags = filters.RelatedFilter("LocalTagFilter", queryset=Tag.objects.all()) - f = PostFilter({'tags': ''}, queryset=Post.objects.all()) - f = f.filters['tags'].filterset + f = PostFilter({"tags": ""}, queryset=Post.objects.all()) + f = f.filters["tags"].filterset - self.assertEqual(f.__module__, 'tests.test_filtering') - self.assertEqual(f.__name__, 'LocalTagFilter') + self.assertEqual(f.__module__, "tests.test_filtering") + self.assertEqual(f.__name__, "LocalTagFilter") def test_empty_param_name(self): - GET = {'': 'foo', 'author': User.objects.get(username='user2').pk} + GET = {"": "foo", "author": User.objects.get(username="user2").pk} f = NoteFilter(GET, queryset=Note.objects.all()) self.assertEqual(len(list(f.qs)), 1) def test_to_field_forwards_relation(self): - GET = {'customer__name': 'Bob Jones'} + GET = {"customer__name": "Bob Jones"} f = AccountFilter(GET) self.assertEqual(len(list(f.qs)), 2) def test_to_field_reverse_relation(self): # Note: pending #99, this query should ideally return 2 distinct results - GET = {'accounts__type': 'c'} + GET = {"accounts__type": "c"} f = CustomerFilter(GET) self.assertEqual(len(list(f.qs)), 3) @@ -524,66 +534,66 @@ class AnnotationTests(TestCase): @classmethod def setUpTestData(cls): - author1 = User.objects.create(username='author1', email='author1@example.org') - author2 = User.objects.create(username='author2', email='author2@example.org') - Post.objects.create(author=author1, content='Post 1', publish_date='2018-01-01') - Post.objects.create(author=author2, content='Post 2', publish_date=None) + author1 = User.objects.create(username="author1", email="author1@example.org") + author2 = User.objects.create(username="author2", email="author2@example.org") + Post.objects.create(author=author1, content="Post 1", publish_date="2018-01-01") + Post.objects.create(author=author2, content="Post 2", publish_date=None) def test_annotation(self): f = PostFilter( - {'is_published': 'true'}, + {"is_published": "true"}, queryset=Post.objects.all(), ) - self.assertEqual([p.content for p in f.qs], ['Post 1']) + self.assertEqual([p.content for p in f.qs], ["Post 1"]) def test_related_annotation(self): f = UserFilter( - {'posts__is_published': 'true'}, + {"posts__is_published": "true"}, queryset=User.objects.all(), ) - self.assertEqual([a.username for a in f.qs], ['author1']) + self.assertEqual([a.username for a in f.qs], ["author1"]) class MiscTests(TestCase): def test_multiwidget_incompatibility(self): - Person.objects.create(name='A') + Person.objects.create(name="A") # test django-filter functionality class PersonFilter(DFFilterSet): - date_joined = filters.DateFromToRangeFilter(field_name='date_joined') + date_joined = filters.DateFromToRangeFilter(field_name="date_joined") class Meta: model = Person - fields = ['date_joined'] + fields = ["date_joined"] # Test from ... to 2016-01-01 GET = { - 'date_joined_before': '2016-01-01', + "date_joined_before": "2016-01-01", } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(f.qs.count(), 0) # test drf-filters caveat class PersonFilter(FilterSet): - date_joined = filters.DateFromToRangeFilter(field_name='date_joined') + date_joined = filters.DateFromToRangeFilter(field_name="date_joined") class Meta: model = Person - fields = ['date_joined'] + fields = ["date_joined"] # Test from ... to 2016-01-01, failure case GET = { - 'date_joined_before': '2016-01-01', + "date_joined_before": "2016-01-01", } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(f.qs.count(), 1) # Test from ... to 2016-01-01, "fix" GET = { - 'date_joined_before': '2016-01-01', - 'date_joined': '', + "date_joined_before": "2016-01-01", + "date_joined": "", } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(f.qs.count(), 0) diff --git a/tests/test_filters.py b/tests/test_filters.py index c3f9052..cc3a759 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -4,11 +4,11 @@ class A(FilterSet): - c = filters.RelatedFilter('tests.test_filters.C') + c = filters.RelatedFilter("tests.test_filters.C") class B(FilterSet): - a = filters.RelatedFilter('A') + a = filters.RelatedFilter("A") class C(FilterSet): @@ -21,19 +21,19 @@ class RelatedFilterFiltersetTests(TestCase): # - Relative `.filterset` imports are durable to inheritance def subclass(self, cls): - return type('Subclass%s' % cls.__name__, (cls, ), {}) + return type("Subclass%s" % cls.__name__, (cls,), {}) def test_filterset_absolute_import(self): for cls in [A, self.subclass(A)]: with self.subTest(cls=cls): - self.assertIs(cls.base_filters['c'].filterset, C) + self.assertIs(cls.base_filters["c"].filterset, C) def test_filterset_relative_import(self): for cls in [B, self.subclass(B)]: with self.subTest(cls=cls): - self.assertIs(cls.base_filters['a'].filterset, A) + self.assertIs(cls.base_filters["a"].filterset, A) def test_filterset_class(self): for cls in [C, self.subclass(C)]: with self.subTest(cls=cls): - self.assertIs(cls.base_filters['b'].filterset, B) + self.assertIs(cls.base_filters["b"].filterset, B) diff --git a/tests/test_filterset.py b/tests/test_filterset.py index 6e511a3..797434b 100644 --- a/tests/test_filterset.py +++ b/tests/test_filterset.py @@ -9,12 +9,11 @@ from rest_framework.views import APIView from rest_framework_filters import FilterSet, filters -from rest_framework_filters.filterset import FilterSetMetaclass, SubsetDisabledMixin +from rest_framework_filters.filterset import (FilterSetMetaclass, + SubsetDisabledMixin) -from .testapp.filters import ( - AFilter, NoteFilter, NoteFilterWithAlias, PersonFilter, PostFilter, TagFilter, - UserFilter, -) +from .testapp.filters import (AFilter, NoteFilter, NoteFilterWithAlias, + PersonFilter, PostFilter, TagFilter, UserFilter) from .testapp.models import Note, Person, Post, Tag, User factory = APIRequestFactory() @@ -35,8 +34,8 @@ class MetaclassTests(TestCase): def test_metamethods(self): functions = [ - 'get_auto_filters', - 'expand_auto_filter', + "get_auto_filters", + "expand_auto_filter", ] for func in functions: @@ -50,12 +49,12 @@ class AutoFilterTests(TestCase): def test_autofilter_not_declared(self): # AutoFilter is not an actual Filter subclass - f = filters.AutoFilter(lookups=['exact']) + f = filters.AutoFilter(lookups=["exact"]) class F(FilterSet): id = f - self.assertEqual(F.auto_filters, {'id': f}) + self.assertEqual(F.auto_filters, {"id": f}) self.assertEqual(F.declared_filters, {}) def test_autofilter_meta_fields_unmodified(self): @@ -64,7 +63,7 @@ def test_autofilter_meta_fields_unmodified(self): f = [] class F(FilterSet): - id = filters.AutoFilter(lookups='__all__') + id = filters.AutoFilter(lookups="__all__") class Meta: model = Note @@ -75,15 +74,15 @@ class Meta: def test_autofilter_replaced(self): # See: https://github.com/philipn/django-rest-framework-filters/issues/118 class F(FilterSet): - id = filters.AutoFilter(lookups=['exact']) + id = filters.AutoFilter(lookups=["exact"]) class Meta: model = Note fields = [] - self.assertEqual(list(F.base_filters), ['id']) - self.assertIsInstance(F.base_filters['id'], filters.NumberFilter) - self.assertEqual(F.base_filters['id'].lookup_expr, 'exact') + self.assertEqual(list(F.base_filters), ["id"]) + self.assertIsInstance(F.base_filters["id"], filters.NumberFilter) + self.assertEqual(F.base_filters["id"].lookup_expr, "exact") def test_autofilter_noop(self): class F(FilterSet): @@ -97,7 +96,7 @@ class Meta: def test_autofilter_with_mixin(self): class Mixin(FilterSet): - title = filters.AutoFilter(lookups=['exact']) + title = filters.AutoFilter(lookups=["exact"]) class Actual(Mixin): class Meta: @@ -113,15 +112,15 @@ class Meta: self.assertEqual(base_filters, {}) base_filters = {name: type(f) for name, f in Actual.base_filters.items()} - self.assertEqual(base_filters, {'title': filters.CharFilter}) + self.assertEqual(base_filters, {"title": filters.CharFilter}) base_filters = {name: type(f) for name, f in Subclass.base_filters.items()} - self.assertEqual(base_filters, {'title': filters.CharFilter}) + self.assertEqual(base_filters, {"title": filters.CharFilter}) def test_autofilter_doesnt_expand_declared(self): # See: https://github.com/philipn/django-rest-framework-filters/issues/234 class F(FilterSet): - pk = filters.AutoFilter(field_name='id', lookups=['exact']) + pk = filters.AutoFilter(field_name="id", lookups=["exact"]) individual = filters.CharFilter() class Meta: @@ -129,31 +128,36 @@ class Meta: fields = [] base_filters = {name: type(f) for name, f in F.base_filters.items()} - self.assertEqual(base_filters, { - 'individual': filters.CharFilter, - 'pk': filters.NumberFilter, - }) + self.assertEqual( + base_filters, + { + "individual": filters.CharFilter, + "pk": filters.NumberFilter, + }, + ) - @unittest.skipIf(django_filters.VERSION < (2, 2), 'requires django-filter 2.2') + @unittest.skipIf(django_filters.VERSION < (2, 2), "requires django-filter 2.2") def test_autofilter_invalid_field(self): msg = "'Meta.fields' must not contain non-model field names: xyz" with self.assertRaisesMessage(TypeError, msg): + class F(FilterSet): - pk = filters.AutoFilter(field_name='xyz', lookups=['exact']) + pk = filters.AutoFilter(field_name="xyz", lookups=["exact"]) class Meta: model = Note fields = [] - @unittest.skipIf(django_filters.VERSION < (2, 2), 'requires django-filter 2.2') + @unittest.skipIf(django_filters.VERSION < (2, 2), "requires django-filter 2.2") def test_all_lookups_invalid_field(self): msg = "'Meta.fields' must not contain non-model field names: xyz" with self.assertRaisesMessage(TypeError, msg): + class F(FilterSet): class Meta: model = Note fields = { - 'xyz': '__all__', + "xyz": "__all__", } def test_relatedfilter_doesnt_expand_declared(self): @@ -161,8 +165,8 @@ def test_relatedfilter_doesnt_expand_declared(self): class F(FilterSet): posts = filters.RelatedFilter( PostFilter, - field_name='post', - lookups=['exact'], + field_name="post", + lookups=["exact"], ) class Meta: @@ -170,9 +174,12 @@ class Meta: fields = [] base_filters = {name: type(f) for name, f in F.base_filters.items()} - self.assertEqual(base_filters, { - 'posts': filters.RelatedFilter, - }) + self.assertEqual( + base_filters, + { + "posts": filters.RelatedFilter, + }, + ) def test_all_lookups_for_relation(self): # See: https://github.com/philipn/django-rest-framework-filters/issues/84 @@ -180,23 +187,25 @@ class F(FilterSet): class Meta: model = Note fields = { - 'author': '__all__', + "author": "__all__", } - self.assertIsInstance(F.base_filters['author'], filters.ModelChoiceFilter) - self.assertIsInstance(F.base_filters['author__in'], BaseInFilter) + self.assertIsInstance(F.base_filters["author"], filters.ModelChoiceFilter) + self.assertIsInstance(F.base_filters["author__in"], BaseInFilter) def test_autofilter_for_related_field(self): # See: https://github.com/philipn/django-rest-framework-filters/issues/127 class F(FilterSet): - author = filters.AutoFilter(field_name='author__last_name', lookups='__all__') + author = filters.AutoFilter( + field_name="author__last_name", lookups="__all__" + ) class Meta: model = Note fields = [] - self.assertIsInstance(F.base_filters['author'], filters.CharFilter) - self.assertEqual(F.base_filters['author'].field_name, 'author__last_name') + self.assertIsInstance(F.base_filters["author"], filters.CharFilter) + self.assertEqual(F.base_filters["author"].field_name, "author__last_name") def test_relatedfilter_combined_with__all__(self): # ensure that related filter is compatible with __all__ lookups. @@ -206,23 +215,23 @@ class F(FilterSet): class Meta: model = Note fields = { - 'author': '__all__', + "author": "__all__", } - self.assertIsInstance(F.base_filters['author'], filters.RelatedFilter) - self.assertIsInstance(F.base_filters['author__in'], BaseInFilter) + self.assertIsInstance(F.base_filters["author"], filters.RelatedFilter) + self.assertIsInstance(F.base_filters["author__in"], BaseInFilter) def test_relatedfilter_lookups(self): # ensure that related filter is compatible with AutoFilter lookups. class F(FilterSet): - author = filters.RelatedFilter(UserFilter, lookups='__all__') + author = filters.RelatedFilter(UserFilter, lookups="__all__") class Meta: model = Note fields = [] - self.assertIsInstance(F.base_filters['author'], filters.RelatedFilter) - self.assertIsInstance(F.base_filters['author__in'], BaseInFilter) + self.assertIsInstance(F.base_filters["author"], filters.RelatedFilter) + self.assertIsInstance(F.base_filters["author__in"], BaseInFilter) def test_relatedfilter_lookups_default(self): class F(FilterSet): @@ -232,20 +241,20 @@ class Meta: model = Note fields = [] - self.assertEqual(len([f for f in F.base_filters if f.startswith('author')]), 1) - self.assertIsInstance(F.base_filters['author'], filters.RelatedFilter) + self.assertEqual(len([f for f in F.base_filters if f.startswith("author")]), 1) + self.assertIsInstance(F.base_filters["author"], filters.RelatedFilter) def test_relatedfilter_lookups_list(self): class F(FilterSet): - author = filters.RelatedFilter(UserFilter, lookups=['in']) + author = filters.RelatedFilter(UserFilter, lookups=["in"]) class Meta: model = Note fields = [] - self.assertEqual(len([f for f in F.base_filters if f.startswith('author')]), 2) - self.assertIsInstance(F.base_filters['author'], filters.RelatedFilter) - self.assertIsInstance(F.base_filters['author__in'], BaseInFilter) + self.assertEqual(len([f for f in F.base_filters if f.startswith("author")]), 2) + self.assertIsInstance(F.base_filters["author"], filters.RelatedFilter) + self.assertIsInstance(F.base_filters["author__in"], BaseInFilter) def test_declared_filter_persistence_with__all__(self): # ensure that __all__ does not overwrite declared filters. @@ -256,30 +265,32 @@ class F(FilterSet): class Meta: model = Person - fields = {'name': '__all__'} + fields = {"name": "__all__"} - self.assertIs(F.base_filters['name'], f) + self.assertIs(F.base_filters["name"], f) def test_declared_filter_persistence_with_autofilter(self): # ensure that AutoFilter does not overwrite declared filters. f = filters.Filter() class F(FilterSet): - id = filters.AutoFilter(lookups='__all__') + id = filters.AutoFilter(lookups="__all__") id__in = f class Meta: model = Note fields = [] - self.assertIs(F.base_filters['id__in'], f) + self.assertIs(F.base_filters["id__in"], f) def test_alllookupsfilter_deprecation_warning(self): - message = ("`AllLookupsFilter()` has been deprecated in " - "favor of `AutoFilter(lookups='__all__')`.") + message = ( + "`AllLookupsFilter()` has been deprecated in " + "favor of `AutoFilter(lookups='__all__')`." + ) with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') + warnings.simplefilter("always") class F(FilterSet): field = filters.AllLookupsFilter() @@ -294,9 +305,13 @@ def external_method(instance, qs, field, value): pass class F(FilterSet): - id = filters.AutoFilter(lookups='__all__', method='filterset_method') - title = filters.AutoFilter(lookups=['exact'], method=external_method) - author = filters.AutoFilter(field_name='author__last_name', lookups='__all__', method='related_method') + id = filters.AutoFilter(lookups="__all__", method="filterset_method") + title = filters.AutoFilter(lookups=["exact"], method=external_method) + author = filters.AutoFilter( + field_name="author__last_name", + lookups="__all__", + method="related_method", + ) class Meta: model = Note @@ -310,8 +325,10 @@ def related_method(self, qs, field, value): for field_name, lookup_filter in F.base_filters.items(): # Ensure field name on filter is overridden to include lookup expression. - if lookup_filter.lookup_expr != 'exact': - self.assertTrue(lookup_filter.field_name.endswith(lookup_filter.lookup_expr)) + if lookup_filter.lookup_expr != "exact": + self.assertTrue( + lookup_filter.field_name.endswith(lookup_filter.lookup_expr) + ) self.assertIsNotNone(lookup_filter.method) self.assertIsNotNone(lookup_filter._method) @@ -325,98 +342,111 @@ def test_not_bound(self): self.assertEqual(len(filtersets), 0) def test_not_related_filter(self): - filtersets = NoteFilter({ - 'title': 'foo', - }).get_related_filtersets() + filtersets = NoteFilter( + { + "title": "foo", + } + ).get_related_filtersets() self.assertEqual(len(filtersets), 0) def test_exact(self): - filtersets = NoteFilter({ - 'author': 'bob', - }).get_related_filtersets() + filtersets = NoteFilter( + { + "author": "bob", + } + ).get_related_filtersets() self.assertEqual(len(filtersets), 1) - self.assertIsInstance(filtersets['author'], UserFilter) + self.assertIsInstance(filtersets["author"], UserFilter) def test_filterset(self): - filtersets = NoteFilter({ - 'author__username': 'bob', - }).get_related_filtersets() + filtersets = NoteFilter( + { + "author__username": "bob", + } + ).get_related_filtersets() self.assertEqual(len(filtersets), 1) - self.assertIsInstance(filtersets['author'], UserFilter) + self.assertIsInstance(filtersets["author"], UserFilter) def test_filterset_alias(self): - filtersets = NoteFilterWithAlias({ - 'writer__username': 'bob', - }).get_related_filtersets() + filtersets = NoteFilterWithAlias( + { + "writer__username": "bob", + } + ).get_related_filtersets() self.assertEqual(len(filtersets), 1) - self.assertIsInstance(filtersets['writer'], UserFilter) + self.assertIsInstance(filtersets["writer"], UserFilter) def test_filterset_twice_removed(self): - filtersets = PostFilter({ - 'note__author__username': 'bob', - }).get_related_filtersets() + filtersets = PostFilter( + { + "note__author__username": "bob", + } + ).get_related_filtersets() self.assertEqual(len(filtersets), 1) - self.assertIsInstance(filtersets['note'], NoteFilter) + self.assertIsInstance(filtersets["note"], NoteFilter) - filtersets = filtersets['note'].get_related_filtersets() + filtersets = filtersets["note"].get_related_filtersets() self.assertEqual(len(filtersets), 1) - self.assertIsInstance(filtersets['author'], UserFilter) + self.assertIsInstance(filtersets["author"], UserFilter) def test_filterset_multiple_filters(self): - filtersets = PostFilter({ - 'note__foo': 'bob', 'tags__bar': 'joe', - }).get_related_filtersets() + filtersets = PostFilter( + { + "note__foo": "bob", + "tags__bar": "joe", + } + ).get_related_filtersets() self.assertEqual(len(filtersets), 2) - self.assertIsInstance(filtersets['note'], NoteFilter) - self.assertIsInstance(filtersets['tags'], TagFilter) + self.assertIsInstance(filtersets["note"], NoteFilter) + self.assertIsInstance(filtersets["tags"], TagFilter) class GetParamFilterNameTests(TestCase): def test_regular_filter(self): - name = UserFilter.get_param_filter_name('email') - self.assertEqual('email', name) + name = UserFilter.get_param_filter_name("email") + self.assertEqual("email", name) def test_exclusion_filter(self): - name = UserFilter.get_param_filter_name('email!') - self.assertEqual('email', name) + name = UserFilter.get_param_filter_name("email!") + self.assertEqual("email", name) def test_non_filter(self): - name = UserFilter.get_param_filter_name('foobar') + name = UserFilter.get_param_filter_name("foobar") self.assertIsNone(name) def test_related_filter(self): # 'exact' matches - name = NoteFilter.get_param_filter_name('author') - self.assertEqual('author', name) + name = NoteFilter.get_param_filter_name("author") + self.assertEqual("author", name) # related attribute filters - name = NoteFilter.get_param_filter_name('author__email') - self.assertEqual('author', name) + name = NoteFilter.get_param_filter_name("author__email") + self.assertEqual("author", name) # non-existent related filters should match, as it's the responsibility # of the related filterset to handle non-existent filters - name = NoteFilter.get_param_filter_name('author__foobar') - self.assertEqual('author', name) + name = NoteFilter.get_param_filter_name("author__foobar") + self.assertEqual("author", name) def test_relationship_regular_filter(self): - name = UserFilter.get_param_filter_name('author__email', rel='author') - self.assertEqual('email', name) + name = UserFilter.get_param_filter_name("author__email", rel="author") + self.assertEqual("email", name) def test_recursive_self_filter(self): - name = PersonFilter.get_param_filter_name('best_friend') - self.assertEqual('best_friend', name) + name = PersonFilter.get_param_filter_name("best_friend") + self.assertEqual("best_friend", name) def test_related_recursive_self_filter(self): # see: https://github.com/philipn/django-rest-framework-filters/issues/333 - name = PersonFilter.get_param_filter_name('best_friend', rel='best_friend') + name = PersonFilter.get_param_filter_name("best_friend", rel="best_friend") self.assertIsNone(name) def test_twice_removed_related_filter(self): @@ -428,47 +458,47 @@ class Meta: model = Post fields = [] - name = PostFilterWithDirectAuthor.get_param_filter_name('note__title') - self.assertEqual('note', name) + name = PostFilterWithDirectAuthor.get_param_filter_name("note__title") + self.assertEqual("note", name) # 'exact' matches, preference more specific filter name, as less specific # filter may not have related access. - name = PostFilterWithDirectAuthor.get_param_filter_name('note__author') - self.assertEqual('note__author', name) + name = PostFilterWithDirectAuthor.get_param_filter_name("note__author") + self.assertEqual("note__author", name) # related attribute filters - name = PostFilterWithDirectAuthor.get_param_filter_name('note__author__email') - self.assertEqual('note__author', name) + name = PostFilterWithDirectAuthor.get_param_filter_name("note__author__email") + self.assertEqual("note__author", name) # non-existent related filters should match, as it's the responsibility # of the related filterset to handle non-existent filters - name = PostFilterWithDirectAuthor.get_param_filter_name('note__author__foobar') - self.assertEqual('note__author', name) + name = PostFilterWithDirectAuthor.get_param_filter_name("note__author__foobar") + self.assertEqual("note__author", name) def test_name_hiding(self): class PostFilterNameHiding(PostFilter): note__author = filters.RelatedFilter(UserFilter) note = filters.RelatedFilter(NoteFilter) - note2 = filters.RelatedFilter(NoteFilter, field_name='note') + note2 = filters.RelatedFilter(NoteFilter, field_name="note") class Meta: model = Post fields = [] - name = PostFilterNameHiding.get_param_filter_name('note__author') - self.assertEqual('note__author', name) + name = PostFilterNameHiding.get_param_filter_name("note__author") + self.assertEqual("note__author", name) - name = PostFilterNameHiding.get_param_filter_name('note__title') - self.assertEqual('note', name) + name = PostFilterNameHiding.get_param_filter_name("note__title") + self.assertEqual("note", name) - name = PostFilterNameHiding.get_param_filter_name('note') - self.assertEqual('note', name) + name = PostFilterNameHiding.get_param_filter_name("note") + self.assertEqual("note", name) - name = PostFilterNameHiding.get_param_filter_name('note2') - self.assertEqual('note2', name) + name = PostFilterNameHiding.get_param_filter_name("note2") + self.assertEqual("note2", name) - name = PostFilterNameHiding.get_param_filter_name('note2__author') - self.assertEqual('note2', name) + name = PostFilterNameHiding.get_param_filter_name("note2__author") + self.assertEqual("note2", name) class GetFilterSubsetTests(TestCase): @@ -483,41 +513,41 @@ class Meta: fields = [] def test_get_subset(self): - filter_subset = self.NoteFilter.get_filter_subset(['title']) + filter_subset = self.NoteFilter.get_filter_subset(["title"]) # ensure that the FilterSet subset only contains the requested fields - self.assertEqual(list(filter_subset), ['title']) + self.assertEqual(list(filter_subset), ["title"]) def test_related_subset(self): # related filters should only return the local RelatedFilter - filter_subset = ['title', 'author', 'author__email'] + filter_subset = ["title", "author", "author__email"] filter_subset = self.NoteFilter.get_filter_subset(filter_subset) - self.assertEqual(list(filter_subset), ['title', 'author']) + self.assertEqual(list(filter_subset), ["title", "author"]) def test_non_filter_subset(self): # non-filter params should be ignored - filter_subset = self.NoteFilter.get_filter_subset(['foobar']) + filter_subset = self.NoteFilter.get_filter_subset(["foobar"]) self.assertEqual(list(filter_subset), []) def test_subset_ordering(self): # sanity check ordering of base filters - filter_subset = ['title', 'author'] + filter_subset = ["title", "author"] filter_subset = [f for f in self.NoteFilter.base_filters if f in filter_subset] - self.assertEqual(list(filter_subset), ['title', 'author']) + self.assertEqual(list(filter_subset), ["title", "author"]) # ensure that the ordering of the subset is the same as the base filters - filter_subset = self.NoteFilter.get_filter_subset(['title', 'author']) - self.assertEqual(list(filter_subset), ['title', 'author']) + filter_subset = self.NoteFilter.get_filter_subset(["title", "author"]) + self.assertEqual(list(filter_subset), ["title", "author"]) # ensure reverse argument order does not change subset ordering - filter_subset = self.NoteFilter.get_filter_subset(['author', 'title']) - self.assertEqual(list(filter_subset), ['title', 'author']) + filter_subset = self.NoteFilter.get_filter_subset(["author", "title"]) + self.assertEqual(list(filter_subset), ["title", "author"]) # ensure related filters do not change subset ordering - filter_subset = ['author__email', 'author', 'title'] + filter_subset = ["author__email", "author", "title"] filter_subset = self.NoteFilter.get_filter_subset(filter_subset) - self.assertEqual(list(filter_subset), ['title', 'author']) + self.assertEqual(list(filter_subset), ["title", "author"]) def test_metaclass_inheritance(self): # See: https://github.com/philipn/django-rest-framework-filters/issues/132 @@ -532,49 +562,49 @@ class NoteFilter(SubFilterSet): class Meta: model = Note - fields = ['title', 'content'] + fields = ["title", "content"] - filter_subset = NoteFilter.get_filter_subset(['author', 'content']) + filter_subset = NoteFilter.get_filter_subset(["author", "content"]) # ensure that the FilterSet subset only contains the requested fields - self.assertEqual(list(filter_subset), ['content', 'author']) + self.assertEqual(list(filter_subset), ["content", "author"]) class DisableSubsetTests(TestCase): class F(FilterSet): class Meta: model = Note - fields = ['author'] + fields = ["author"] def test_unbound_subset(self): F = self.F.disable_subset() self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(F().filters), ['author']) + self.assertEqual(list(F().filters), ["author"]) def test_bound_subset(self): F = self.F.disable_subset() self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(F({}).filters), ['author']) - self.assertEqual(list(F({'author': ''}).filters), ['author']) + self.assertEqual(list(F({}).filters), ["author"]) + self.assertEqual(list(F({"author": ""}).filters), ["author"]) def test_duplicate_disable(self): F = self.F.disable_subset().disable_subset() self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(F({}).filters), ['author']) + self.assertEqual(list(F({}).filters), ["author"]) def test_subset_form(self): # test that subset-enabled forms only have provided fields F = self.F self.assertFalse(issubclass(F, SubsetDisabledMixin)) self.assertEqual(list(F({}).form.fields), []) - self.assertEqual(list(F({'author': ''}).form.fields), ['author']) + self.assertEqual(list(F({"author": ""}).form.fields), ["author"]) def test_subset_disabled_form(self): # test that subset-disabled forms have all fields F = self.F.disable_subset() self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(F({}).form.fields), ['author']) - self.assertEqual(list(F({'author': ''}).form.fields), ['author']) + self.assertEqual(list(F({}).form.fields), ["author"]) + self.assertEqual(list(F({"author": ""}).form.fields), ["author"]) class DisableSubsetRecursiveTests(TestCase): @@ -585,11 +615,11 @@ def test_depth0(self): # 0-depth disabled self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(f.filters), ['title', 'b']) + self.assertEqual(list(f.filters), ["title", "b"]) # 1-depth not disabled - F = f.filters['b'].filterset - f = f.related_filtersets['b'] + F = f.filters["b"].filterset + f = f.related_filtersets["b"] self.assertFalse(issubclass(F, SubsetDisabledMixin)) self.assertEqual(list(f.filters), []) @@ -599,17 +629,17 @@ def test_depth1(self): # 0-depth disabled self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(f.filters), ['title', 'b']) + self.assertEqual(list(f.filters), ["title", "b"]) # 1-depth disabled - F = f.filters['b'].filterset - f = f.related_filtersets['b'] + F = f.filters["b"].filterset + f = f.related_filtersets["b"] self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(f.filters), ['name', 'c']) + self.assertEqual(list(f.filters), ["name", "c"]) # 2-depth not disabled - F = f.filters['c'].filterset - f = f.related_filtersets['c'] + F = f.filters["c"].filterset + f = f.related_filtersets["c"] self.assertFalse(issubclass(F, SubsetDisabledMixin)) self.assertEqual(list(f.filters), []) @@ -619,23 +649,23 @@ def test_depth2(self): # 0-depth disabled self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(f.filters), ['title', 'b']) + self.assertEqual(list(f.filters), ["title", "b"]) # 1-depth disabled - F = f.filters['b'].filterset - f = f.related_filtersets['b'] + F = f.filters["b"].filterset + f = f.related_filtersets["b"] self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(f.filters), ['name', 'c']) + self.assertEqual(list(f.filters), ["name", "c"]) # 2-depth disabled - F = f.filters['c'].filterset - f = f.related_filtersets['c'] + F = f.filters["c"].filterset + f = f.related_filtersets["c"] self.assertTrue(issubclass(F, SubsetDisabledMixin)) - self.assertEqual(list(f.filters), ['title', 'a']) + self.assertEqual(list(f.filters), ["title", "a"]) # 3-depth not disabled - F = f.filters['a'].filterset - f = f.related_filtersets['a'] + F = f.filters["a"].filterset + f = f.related_filtersets["a"] self.assertFalse(issubclass(F, SubsetDisabledMixin)) self.assertEqual(list(f.filters), []) @@ -648,12 +678,12 @@ class FilterExclusionTests(TestCase): @classmethod def setUpTestData(cls): - t1 = Tag.objects.create(name='Tag 1') - t2 = Tag.objects.create(name='Tag 2') - t3 = Tag.objects.create(name='Something else entirely') + t1 = Tag.objects.create(name="Tag 1") + t2 = Tag.objects.create(name="Tag 2") + t3 = Tag.objects.create(name="Something else entirely") - p1 = Post.objects.create(title='Post 1', content='content 1') - p2 = Post.objects.create(title='Post 2', content='content 2') + p1 = Post.objects.create(title="Post 1", content="content 1") + p2 = Post.objects.create(title="Post 2", content="content 2") p1.tags.set([t1, t2]) p2.tags.set([t3]) @@ -661,70 +691,72 @@ def setUpTestData(cls): def test_exclude_property(self): # Ensure that the filter is set to exclude GET = { - 'name__contains!': 'Tag', + "name__contains!": "Tag", } filterset = TagFilter(GET, queryset=Tag.objects.all()) - self.assertTrue(filterset.filters['name__contains!'].exclude) + self.assertTrue(filterset.filters["name__contains!"].exclude) def test_filter_and_exclude(self): # Ensure that both the filter and exclusion filter are available GET = { - 'name__contains': 'Tag', - 'name__contains!': 'Tag', + "name__contains": "Tag", + "name__contains!": "Tag", } filterset = TagFilter(GET, queryset=Tag.objects.all()) - self.assertFalse(filterset.filters['name__contains'].exclude) - self.assertTrue(filterset.filters['name__contains!'].exclude) + self.assertFalse(filterset.filters["name__contains"].exclude) + self.assertTrue(filterset.filters["name__contains!"].exclude) def test_related_exclude(self): GET = { - 'tags__name__contains!': 'Tag', + "tags__name__contains!": "Tag", } filterset = PostFilter(GET, queryset=Post.objects.all()) - filterset = filterset.related_filtersets['tags'] + filterset = filterset.related_filtersets["tags"] - self.assertTrue(filterset.filters['name__contains!'].exclude) + self.assertTrue(filterset.filters["name__contains!"].exclude) def test_exclusion_results(self): GET = { - 'name__contains!': 'Tag', + "name__contains!": "Tag", } filterset = TagFilter(GET, queryset=Tag.objects.all()) results = [r.name for r in filterset.qs] self.assertEqual(len(results), 1) - self.assertEqual(results[0], 'Something else entirely') + self.assertEqual(results[0], "Something else entirely") def test_filter_and_exclusion_results(self): GET = { - 'name__contains': 'Tag', - 'name__contains!': '2', + "name__contains": "Tag", + "name__contains!": "2", } filterset = TagFilter(GET, queryset=Tag.objects.all()) results = [r.name for r in filterset.qs] self.assertEqual(len(results), 1) - self.assertEqual(results[0], 'Tag 1') + self.assertEqual(results[0], "Tag 1") def test_related_exclusion_results(self): GET = { - 'tags__name__contains!': 'Tag', + "tags__name__contains!": "Tag", } filterset = PostFilter(GET, queryset=Post.objects.all()) results = [r.title for r in filterset.qs] self.assertEqual(len(results), 1) - self.assertEqual(results[0], 'Post 2') + self.assertEqual(results[0], "Post 2") def test_exclude_and_request_interaction(self): # See: https://github.com/philipn/django-rest-framework-filters/issues/171 - request = APIView().initialize_request(factory.get('/?tags__name__contains!=Tag')) + request = APIView().initialize_request( + factory.get("/?tags__name__contains!=Tag") + ) filterset = PostFilter( request.query_params, request=request, @@ -735,9 +767,9 @@ def test_exclude_and_request_interaction(self): with limit_recursion(): qs = filterset.qs except RuntimeError: - self.fail('Recursion limit reached') + self.fail("Recursion limit reached") results = [r.title for r in qs] self.assertEqual(len(results), 1) - self.assertEqual(results[0], 'Post 2') + self.assertEqual(results[0], "Post 2") diff --git a/tests/test_forms.py b/tests/test_forms.py index dc7e6dd..8879a7b 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -26,17 +26,17 @@ def test_subset_disabled_form_fields(self): class F(FilterSet): class Meta: model = Post - fields = ['title', 'content'] + fields = ["title", "content"] F = F.disable_subset() form = F({}).form - self.assertEqual(list(form.fields), ['title', 'content']) + self.assertEqual(list(form.fields), ["title", "content"]) def test_unbound_form_fields(self): class F(FilterSet): class Meta: model = Post - fields = ['title', 'content'] + fields = ["title", "content"] form = F().form self.assertEqual(list(form.fields), []) @@ -45,40 +45,45 @@ def test_bound_form_fields(self): class F(FilterSet): class Meta: model = Post - fields = ['title', 'content'] + fields = ["title", "content"] form = F({}).form self.assertEqual(list(form.fields), []) - form = F({'title': 'foo'}).form - self.assertEqual(list(form.fields), ['title']) + form = F({"title": "foo"}).form + self.assertEqual(list(form.fields), ["title"]) def test_related_form_fields(self): # FilterSet form should not contain fields from related filtersets class F(FilterSet): author = filters.RelatedFilter( - 'tests.testapp.filters.UserFilter', + "tests.testapp.filters.UserFilter", queryset=User.objects.all(), ) class Meta: model = Post - fields = ['title', 'author'] + fields = ["title", "author"] - f = F({'title': '', 'author': '', 'author__email': ''}) + f = F({"title": "", "author": "", "author__email": ""}) form = f.form - self.assertEqual(list(form.fields), ['title', 'author']) + self.assertEqual(list(form.fields), ["title", "author"]) - form = f.related_filtersets['author'].form - self.assertEqual(list(form.fields), ['email']) + form = f.related_filtersets["author"].form + self.assertEqual(list(form.fields), ["email"]) def test_validation_errors(self): - f = PostFilter({ - 'publish_date__year': 'foo', - 'author__last_login__date': 'bar', - }) - self.assertEqual(f.form.errors, { - 'publish_date__year': ['Enter a number.'], - 'author__last_login__date': ['Enter a valid date.'], - }) + f = PostFilter( + { + "publish_date__year": "foo", + "author__last_login__date": "bar", + } + ) + self.assertEqual( + f.form.errors, + { + "publish_date__year": ["Enter a number."], + "author__last_login__date": ["Enter a valid date."], + }, + ) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 925fb3c..12d79c4 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -30,13 +30,13 @@ def add_timedelta(time, timedelta): class PersonSerializer(serializers.ModelSerializer): class Meta: model = Person - fields = ['date_joined', 'time_joined', 'datetime_joined'] + fields = ["date_joined", "time_joined", "datetime_joined"] class PersonFilter(FilterSet): - date_joined = AutoFilter(field_name='date_joined', lookups='__all__') - time_joined = AutoFilter(field_name='time_joined', lookups='__all__') - datetime_joined = AutoFilter(field_name='datetime_joined', lookups='__all__') + date_joined = AutoFilter(field_name="date_joined", lookups="__all__") + time_joined = AutoFilter(field_name="time_joined", lookups="__all__") + datetime_joined = AutoFilter(field_name="datetime_joined", lookups="__all__") class Meta: model = Person @@ -44,8 +44,8 @@ class Meta: class InLookupPersonFilter(FilterSet): - pk = AutoFilter('id', lookups='__all__') - name = AutoFilter('name', lookups='__all__') + pk = AutoFilter("id", lookups="__all__") + name = AutoFilter("name", lookups="__all__") class Meta: model = Person @@ -60,7 +60,9 @@ def setUpTestData(cls): # Created at least one second apart mark = Person.objects.create(name="Mark", best_friend=john) - mark.time_joined = add_timedelta(mark.time_joined, datetime.timedelta(seconds=1)) + mark.time_joined = add_timedelta( + mark.time_joined, datetime.timedelta(seconds=1) + ) mark.datetime_joined += datetime.timedelta(seconds=1) mark.save() @@ -73,21 +75,23 @@ def test_implicit_date_filters(self): # serializer output. data = PersonSerializer(john).data - date_str = JSONRenderer().render(data['date_joined']).decode('utf-8').strip('"') + date_str = JSONRenderer().render(data["date_joined"]).decode("utf-8").strip('"') # Adjust for imprecise rendering of time - offset = parse_datetime(data['datetime_joined']) + datetime.timedelta(seconds=0.6) + offset = parse_datetime(data["datetime_joined"]) + datetime.timedelta( + seconds=0.6 + ) datetime_str = JSONRenderer().render(offset) - datetime_str = datetime_str.decode('utf-8').strip('"') + datetime_str = datetime_str.decode("utf-8").strip('"') # Adjust for imprecise rendering of time - offset = datetime.datetime.combine(today, parse_time(data['time_joined'])) + offset = datetime.datetime.combine(today, parse_time(data["time_joined"])) offset += datetime.timedelta(seconds=0.6) - time_str = JSONRenderer().render(offset.time()).decode('utf-8').strip('"') + time_str = JSONRenderer().render(offset.time()).decode("utf-8").strip('"') # DateField GET = { - 'date_joined__lte': date_str, + "date_joined__lte": date_str, } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(len(list(f.qs)), 2) @@ -95,7 +99,7 @@ def test_implicit_date_filters(self): # DateTimeField GET = { - 'datetime_joined__lte': datetime_str, + "datetime_joined__lte": datetime_str, } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -104,30 +108,32 @@ def test_implicit_date_filters(self): # TimeField GET = { - 'time_joined__lte': time_str, + "time_joined__lte": time_str, } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(len(list(f.qs)), 1) p = list(f.qs)[0] self.assertEqual(p.name, "John") - @override_settings(USE_TZ=True, TIME_ZONE='UTC') + @override_settings(USE_TZ=True, TIME_ZONE="UTC") def test_datetime_timezone_awareness(self): # Issue #24 - coorectly handle datetime strings terminating in 'Z'. # Figure out what the date strings should look like based on the serializer output john = Person.objects.get(name="John") data = PersonSerializer(john).data - offset = parse_datetime(data['datetime_joined']) + datetime.timedelta(seconds=0.6) + offset = parse_datetime(data["datetime_joined"]) + datetime.timedelta( + seconds=0.6 + ) datetime_str = JSONRenderer().render(offset) - datetime_str = datetime_str.decode('utf-8').strip('"') + datetime_str = datetime_str.decode("utf-8").strip('"') # DRF appends a 'Z' to timezone aware UTC datetimes when rendering: # https://github.com/tomchristie/django-rest-framework/blob/3.2.0/rest_framework/fields.py#L1002-L1006 - self.assertTrue(datetime_str.endswith('Z')) + self.assertTrue(datetime_str.endswith("Z")) GET = { - 'datetime_joined__lte': datetime_str, + "datetime_joined__lte": datetime_str, } f = PersonFilter(GET, queryset=Person.objects.all()) self.assertEqual(len(list(f.qs)), 1) @@ -139,56 +145,58 @@ class BooleanFilterTests(TestCase): @classmethod def setUpTestData(cls): - User.objects.create(username="user1", - email="user1@example.org", - is_active=True, - last_login=today) - User.objects.create(username="user2", - email="user2@example.org", - is_active=False) + User.objects.create( + username="user1", + email="user1@example.org", + is_active=True, + last_login=today, + ) + User.objects.create( + username="user2", email="user2@example.org", is_active=False + ) def test_boolean_filter(self): # Capitalized True - GET = {'is_active': 'True'} + GET = {"is_active": "True"} filterset = UserFilter(GET, queryset=User.objects.all()) results = list(filterset.qs) self.assertEqual(len(results), 1) - self.assertEqual(results[0].username, 'user1') + self.assertEqual(results[0].username, "user1") # Lowercase True - GET = {'is_active': 'true'} + GET = {"is_active": "true"} filterset = UserFilter(GET, queryset=User.objects.all()) results = list(filterset.qs) self.assertEqual(len(results), 1) - self.assertEqual(results[0].username, 'user1') + self.assertEqual(results[0].username, "user1") # Uppercase True - GET = {'is_active': 'TRUE'} + GET = {"is_active": "TRUE"} filterset = UserFilter(GET, queryset=User.objects.all()) results = list(filterset.qs) self.assertEqual(len(results), 1) - self.assertEqual(results[0].username, 'user1') + self.assertEqual(results[0].username, "user1") # Capitalized False - GET = {'is_active': 'False'} + GET = {"is_active": "False"} filterset = UserFilter(GET, queryset=User.objects.all()) results = list(filterset.qs) self.assertEqual(len(results), 1) - self.assertEqual(results[0].username, 'user2') + self.assertEqual(results[0].username, "user2") # Lowercase False - GET = {'is_active': 'false'} + GET = {"is_active": "false"} filterset = UserFilter(GET, queryset=User.objects.all()) results = list(filterset.qs) self.assertEqual(len(results), 1) - self.assertEqual(results[0].username, 'user2') + self.assertEqual(results[0].username, "user2") # Uppercase False - GET = {'is_active': 'FALSE'} + GET = {"is_active": "FALSE"} filterset = UserFilter(GET, queryset=User.objects.all()) results = list(filterset.qs) self.assertEqual(len(results), 1) - self.assertEqual(results[0].username, 'user2') + self.assertEqual(results[0].username, "user2") class InLookupTests(TestCase): @@ -198,20 +206,22 @@ def setUpTestData(cls): john = Person.objects.create(name="John") Person.objects.create(name="Mark", best_friend=john) - User.objects.create(username="user1", - email="user1@example.org", - is_active=True, - last_login=today) - User.objects.create(username="user2", - email="user2@example.org", - is_active=False) + User.objects.create( + username="user1", + email="user1@example.org", + is_active=True, + last_login=today, + ) + User.objects.create( + username="user2", email="user2@example.org", is_active=False + ) def test_inset_number_filter(self): p1 = Person.objects.get(name="John").pk p2 = Person.objects.get(name="Mark").pk ALL_GET = { - 'pk__in': '{:d},{:d}'.format(p1, p2), + "pk__in": "{:d},{:d}".format(p1, p2), } f = InLookupPersonFilter(ALL_GET, queryset=Person.objects.all()) f = [x.pk for x in f.qs] @@ -220,14 +230,14 @@ def test_inset_number_filter(self): self.assertIn(p2, f) INVALID_GET = { - 'pk__in': '{:d},c{:d}'.format(p1, p2), + "pk__in": "{:d},c{:d}".format(p1, p2), } f = InLookupPersonFilter(INVALID_GET, queryset=Person.objects.all()) self.assertFalse(f.is_valid()) self.assertEqual(f.qs.count(), 2) EXTRA_GET = { - 'pk__in': '{:d},{:d},{:d}'.format(p1, p2, p1 * p2), + "pk__in": "{:d},{:d},{:d}".format(p1, p2, p1 * p2), } f = InLookupPersonFilter(EXTRA_GET, queryset=Person.objects.all()) f = [x.pk for x in f.qs] @@ -236,7 +246,7 @@ def test_inset_number_filter(self): self.assertIn(p2, f) DISORDERED_GET = { - 'pk__in': '{:d},{:d},{:d}'.format(p2, p2 * p1, p1), + "pk__in": "{:d},{:d},{:d}".format(p2, p2 * p1, p1), } f = InLookupPersonFilter(DISORDERED_GET, queryset=Person.objects.all()) f = [x.pk for x in f.qs] @@ -249,7 +259,7 @@ def test_inset_char_filter(self): p2 = Person.objects.get(name="Mark").name ALL_GET = { - 'name__in': '{},{}'.format(p1, p2), + "name__in": "{},{}".format(p1, p2), } f = InLookupPersonFilter(ALL_GET, queryset=Person.objects.all()) f = [x.name for x in f.qs] @@ -258,13 +268,13 @@ def test_inset_char_filter(self): self.assertIn(p2, f) NONEXISTENT_GET = { - 'name__in': '{},Foo{}'.format(p1, p2), + "name__in": "{},Foo{}".format(p1, p2), } f = InLookupPersonFilter(NONEXISTENT_GET, queryset=Person.objects.all()) self.assertEqual(len(list(f.qs)), 1) EXTRA_GET = { - 'name__in': '{},{},{}'.format(p1, p2, p1 + p2), + "name__in": "{},{},{}".format(p1, p2, p1 + p2), } f = InLookupPersonFilter(EXTRA_GET, queryset=Person.objects.all()) f = [x.name for x in f.qs] @@ -273,7 +283,7 @@ def test_inset_char_filter(self): self.assertIn(p2, f) DISORDERED_GET = { - 'name__in': '{},{},{}'.format(p2, p2 + p1, p1), + "name__in": "{},{},{}".format(p2, p2 + p1, p1), } f = InLookupPersonFilter(DISORDERED_GET, queryset=Person.objects.all()) f = [x.name for x in f.qs] @@ -286,37 +296,39 @@ class IsNullLookupTests(TestCase): @classmethod def setUpTestData(cls): - User.objects.create(username="user1", - email="user1@example.org", - is_active=True, - last_login=today) - User.objects.create(username="user2", - email="user2@example.org", - is_active=False) + User.objects.create( + username="user1", + email="user1@example.org", + is_active=True, + last_login=today, + ) + User.objects.create( + username="user2", email="user2@example.org", is_active=False + ) def test_isnull_override(self): import django_filters.filters self.assertIsInstance( - UserFilter.base_filters['last_login__isnull'], + UserFilter.base_filters["last_login__isnull"], django_filters.filters.BooleanFilter, ) - GET = {'last_login__isnull': 'false'} + GET = {"last_login__isnull": "false"} filterset = UserFilter(GET, queryset=User.objects.all()) - filter_ = filterset.filters['last_login__isnull'] + filter_ = filterset.filters["last_login__isnull"] self.assertIsInstance(filter_, django_filters.filters.BooleanFilter) results = list(filterset.qs) self.assertEqual(len(results), 1) - self.assertEqual(results[0].username, 'user1') + self.assertEqual(results[0].username, "user1") - GET = {'last_login__isnull': 'true'} + GET = {"last_login__isnull": "true"} filterset = UserFilter(GET, queryset=User.objects.all()) - filter_ = filterset.filters['last_login__isnull'] + filter_ = filterset.filters["last_login__isnull"] self.assertIsInstance(filter_, django_filters.filters.BooleanFilter) results = list(filterset.qs) self.assertEqual(len(results), 1) - self.assertEqual(results[0].username, 'user2') + self.assertEqual(results[0].username, "user2") class FilterMethodTests(TestCase): @@ -325,20 +337,24 @@ class FilterMethodTests(TestCase): def setUpTestData(cls): user = User.objects.create(username="user1", email="user1@example.org") - note1 = Note.objects.create(title="Test 1", content="Test content 1", author=user) - note2 = Note.objects.create(title="Test 2", content="Test content 2", author=user) + note1 = Note.objects.create( + title="Test 1", content="Test content 1", author=user + ) + note2 = Note.objects.create( + title="Test 2", content="Test content 2", author=user + ) post1 = Post.objects.create(note=note1, content="Test content in post 1") - post2 = Post.objects.create(note=note2, - content="Test content in post 2", - publish_date=today) + post2 = Post.objects.create( + note=note2, content="Test content in post 2", publish_date=today + ) Cover.objects.create(post=post1, comment="Cover 1") Cover.objects.create(post=post2, comment="Cover 2") def test_method_filter(self): GET = { - 'is_published': 'true', + "is_published": "true", } filterset = PostFilter(GET, queryset=Post.objects.all()) results = list(filterset.qs) @@ -348,7 +364,7 @@ def test_method_filter(self): def test_related_method_filter(self): # Missing MethodFilter methods are silently ignored, returning the unfiltered qs. GET = { - 'post__is_published': 'true', + "post__is_published": "true", } filterset = CoverFilter(GET, queryset=Cover.objects.all()) results = list(filterset.qs) diff --git a/tests/test_utils.py b/tests/test_utils.py index 878075f..0877576 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,38 +7,38 @@ class LookupsForFieldTests(TestCase): def test_standard_field(self): - model_field = Person._meta.get_field('name') + model_field = Person._meta.get_field("name") lookups = utils.lookups_for_field(model_field) - self.assertIn('exact', lookups) - self.assertNotIn('year', lookups) - self.assertNotIn('date', lookups) + self.assertIn("exact", lookups) + self.assertNotIn("year", lookups) + self.assertNotIn("date", lookups) def test_transformed_field(self): - model_field = Person._meta.get_field('datetime_joined') + model_field = Person._meta.get_field("datetime_joined") lookups = utils.lookups_for_field(model_field) - self.assertIn('exact', lookups) - self.assertIn('year__exact', lookups) - self.assertIn('date__year__exact', lookups) + self.assertIn("exact", lookups) + self.assertIn("year__exact", lookups) + self.assertIn("date__year__exact", lookups) def test_relation_field(self): # ForeignObject relations are special cased currently - model_field = Note._meta.get_field('author') + model_field = Note._meta.get_field("author") lookups = utils.lookups_for_field(model_field) - self.assertIn('exact', lookups) - self.assertIn('in', lookups) - self.assertNotIn('regex', lookups) + self.assertIn("exact", lookups) + self.assertIn("in", lookups) + self.assertNotIn("regex", lookups) class LookupsForTransformTests(TestCase): def test_recursion_prevention(self): - model_field = Person._meta.get_field('name') + model_field = Person._meta.get_field("name") lookups = utils.lookups_for_field(model_field) - self.assertIn('unaccent__exact', lookups) - self.assertNotIn('unaccent__unaccent__exact', lookups) + self.assertIn("unaccent__exact", lookups) + self.assertNotIn("unaccent__unaccent__exact", lookups) class LookaheadTests(TestCase): @@ -52,8 +52,11 @@ def test_single(self): def test_multiple(self): result = list(utils.lookahead([1, 2, 3])) - self.assertListEqual(result, [ - (1, True), - (2, True), - (3, False), - ]) + self.assertListEqual( + result, + [ + (1, True), + (2, True), + (3, False), + ], + ) diff --git a/tests/testapp/apps.py b/tests/testapp/apps.py index eafad9f..1a0e60b 100644 --- a/tests/testapp/apps.py +++ b/tests/testapp/apps.py @@ -1,4 +1,3 @@ - from django.apps import AppConfig from django.db.models import CharField, TextField @@ -6,7 +5,7 @@ class TestappConfig(AppConfig): - name = 'tests.testapp' + name = "tests.testapp" def ready(self): CharField.register_lookup(Unaccent) diff --git a/tests/testapp/filters.py b/tests/testapp/filters.py index ca05134..7b41df0 100644 --- a/tests/testapp/filters.py +++ b/tests/testapp/filters.py @@ -1,30 +1,28 @@ - import django_filters from rest_framework_filters import filters from rest_framework_filters.filters import AutoFilter, RelatedFilter from rest_framework_filters.filterset import FilterSet -from .models import ( - A, Account, B, Blog, C, Cover, Customer, Note, Page, Person, Post, Tag, User, -) +from .models import (A, Account, B, Blog, C, Cover, Customer, Note, Page, + Person, Post, Tag, User) class DFUserFilter(django_filters.FilterSet): - email = filters.CharFilter(field_name='email') + email = filters.CharFilter(field_name="email") class Meta: model = User - fields = '__all__' + fields = "__all__" class UserFilter(FilterSet): - username = AutoFilter(field_name='username', lookups='__all__') - email = filters.CharFilter(field_name='email') - last_login = AutoFilter(lookups='__all__') - is_active = filters.BooleanFilter(field_name='is_active') + username = AutoFilter(field_name="username", lookups="__all__") + email = filters.CharFilter(field_name="email") + last_login = AutoFilter(lookups="__all__") + is_active = filters.BooleanFilter(field_name="is_active") - posts = RelatedFilter('PostFilter', field_name='post', queryset=Post.objects.all()) + posts = RelatedFilter("PostFilter", field_name="post", queryset=Post.objects.all()) class Meta: model = User @@ -32,7 +30,7 @@ class Meta: class NoteFilter(FilterSet): - title = AutoFilter(field_name='title', lookups='__all__') + title = AutoFilter(field_name="title", lookups="__all__") author = RelatedFilter(UserFilter, queryset=User.objects.all()) class Meta: @@ -41,7 +39,7 @@ class Meta: class TagFilter(FilterSet): - name = AutoFilter(field_name='name', lookups='__all__') + name = AutoFilter(field_name="name", lookups="__all__") class Meta: model = Tag @@ -49,8 +47,8 @@ class Meta: class BlogFilter(FilterSet): - name = AutoFilter(field_name='name', lookups='__all__') - post = RelatedFilter('PostFilter', queryset=Post.objects.all()) + name = AutoFilter(field_name="name", lookups="__all__") + post = RelatedFilter("PostFilter", queryset=Post.objects.all()) class Meta: model = Blog @@ -59,10 +57,10 @@ class Meta: class PostFilter(FilterSet): # Used for Related filter and Filter.method regression tests - title = filters.AutoFilter(field_name='title', lookups='__all__') + title = filters.AutoFilter(field_name="title", lookups="__all__") - publish_date = filters.AutoFilter(lookups='__all__') - is_published = filters.BooleanFilter(method='filter_is_published') + publish_date = filters.AutoFilter(lookups="__all__") + is_published = filters.BooleanFilter(method="filter_is_published") author = RelatedFilter(UserFilter, queryset=User.objects.all()) note = RelatedFilter(NoteFilter, queryset=Note.objects.all()) @@ -77,12 +75,11 @@ def filter_is_published(self, queryset, field_name, value): # then the post is not published. This filter method demonstrates annotations. # Note: don't modify this without updating test_filtering.AnnotationTests - return queryset.annotate_is_published() \ - .filter(**{field_name: value}) + return queryset.annotate_is_published().filter(**{field_name: value}) class CoverFilter(FilterSet): - comment = filters.CharFilter(field_name='comment') + comment = filters.CharFilter(field_name="comment") post = RelatedFilter(PostFilter, queryset=Post.objects.all()) class Meta: @@ -91,15 +88,15 @@ class Meta: class PageFilter(FilterSet): - title = filters.CharFilter(field_name='title') + title = filters.CharFilter(field_name="title") previous_page = RelatedFilter( PostFilter, - field_name='previous_page', + field_name="previous_page", queryset=Post.objects.all(), ) two_pages_back = RelatedFilter( PostFilter, - field_name='previous_page__previous_page', + field_name="previous_page__previous_page", queryset=Page.objects.all(), ) @@ -111,7 +108,7 @@ class Meta: ################################################################################ # Aliased parameter names ###################################################### class UserFilterWithAlias(FilterSet): - name = filters.CharFilter(field_name='username') + name = filters.CharFilter(field_name="username") class Meta: model = User @@ -119,8 +116,8 @@ class Meta: class NoteFilterWithAlias(FilterSet): - title = filters.CharFilter(field_name='title') - writer = RelatedFilter(UserFilter, field_name='author', queryset=User.objects.all()) + title = filters.CharFilter(field_name="title") + writer = RelatedFilter(UserFilter, field_name="author", queryset=User.objects.all()) class Meta: model = Note @@ -138,8 +135,8 @@ class Meta: ################################################################################ # Recursive filtersets ######################################################### class AFilter(FilterSet): - title = filters.CharFilter(field_name='title') - b = RelatedFilter('BFilter', field_name='b', queryset=B.objects.all()) + title = filters.CharFilter(field_name="title") + b = RelatedFilter("BFilter", field_name="b", queryset=B.objects.all()) class Meta: model = A @@ -147,8 +144,8 @@ class Meta: class BFilter(FilterSet): - name = filters.CharFilter(field_name='name') - c = RelatedFilter('CFilter', field_name='c', queryset=C.objects.all()) + name = filters.CharFilter(field_name="name") + c = RelatedFilter("CFilter", field_name="c", queryset=C.objects.all()) class Meta: model = B @@ -156,8 +153,8 @@ class Meta: class CFilter(FilterSet): - title = filters.CharFilter(field_name='title') - a = RelatedFilter('AFilter', field_name='a', queryset=A.objects.all()) + title = filters.CharFilter(field_name="title") + a = RelatedFilter("AFilter", field_name="a", queryset=A.objects.all()) class Meta: model = C @@ -165,10 +162,10 @@ class Meta: class PersonFilter(FilterSet): - name = AutoFilter(field_name='name', lookups='__all__') + name = AutoFilter(field_name="name", lookups="__all__") best_friend = RelatedFilter( - 'tests.testapp.filters.PersonFilter', - field_name='best_friend', + "tests.testapp.filters.PersonFilter", + field_name="best_friend", queryset=Person.objects.all(), ) @@ -181,23 +178,23 @@ class Meta: # `to_field` filtersets ######################################################## class CustomerFilter(FilterSet): accounts = RelatedFilter( - 'AccountFilter', - field_name='account', + "AccountFilter", + field_name="account", queryset=Account.objects.all(), ) class Meta: model = Customer - fields = ['name', 'ssn', 'dob', 'accounts'] + fields = ["name", "ssn", "dob", "accounts"] class AccountFilter(FilterSet): customer = RelatedFilter( - 'CustomerFilter', - to_field_name='ssn', + "CustomerFilter", + to_field_name="ssn", queryset=Customer.objects.all(), ) class Meta: model = Account - fields = ['customer', 'type', 'name'] + fields = ["customer", "type", "name"] diff --git a/tests/testapp/lookups.py b/tests/testapp/lookups.py index 235c17c..f8bb5c7 100644 --- a/tests/testapp/lookups.py +++ b/tests/testapp/lookups.py @@ -5,5 +5,5 @@ # This is necessary as the postgres app requires psycopg2 to be installed. class Unaccent(Transform): bilateral = True - lookup_name = 'unaccent' - function = 'UNACCENT' + lookup_name = "unaccent" + function = "UNACCENT" diff --git a/tests/testapp/migrations/0001_initial.py b/tests/testapp/migrations/0001_initial.py index 570c914..2fab9ed 100644 --- a/tests/testapp/migrations/0001_initial.py +++ b/tests/testapp/migrations/0001_initial.py @@ -1,8 +1,8 @@ # Generated by Django 2.2.2 on 2019-08-14 20:04 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -15,113 +15,283 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='A', + name="A", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), ], ), migrations.CreateModel( - name='Blog', + name="Blog", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( - name='Customer', + name="Customer", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=80)), - ('ssn', models.CharField(max_length=9, unique=True)), - ('dob', models.DateField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=80)), + ("ssn", models.CharField(max_length=9, unique=True)), + ("dob", models.DateField()), ], ), migrations.CreateModel( - name='Note', + name="Note", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('content', models.TextField()), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ("content", models.TextField()), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='Tag', + name="Tag", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( - name='Post', + name="Post", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('content', models.TextField()), - ('publish_date', models.DateField(null=True)), - ('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('blog', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.Blog')), - ('note', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.Note')), - ('tags', models.ManyToManyField(to='testapp.Tag')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ("content", models.TextField()), + ("publish_date", models.DateField(null=True)), + ( + "author", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "blog", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="testapp.Blog", + ), + ), + ( + "note", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="testapp.Note", + ), + ), + ("tags", models.ManyToManyField(to="testapp.Tag")), ], ), migrations.CreateModel( - name='Person', + name="Person", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('date_joined', models.DateField(auto_now_add=True)), - ('time_joined', models.TimeField(auto_now_add=True)), - ('datetime_joined', models.DateTimeField(auto_now_add=True)), - ('best_friend', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.Person')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("date_joined", models.DateField(auto_now_add=True)), + ("time_joined", models.TimeField(auto_now_add=True)), + ("datetime_joined", models.DateTimeField(auto_now_add=True)), + ( + "best_friend", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="testapp.Person", + ), + ), ], ), migrations.CreateModel( - name='Page', + name="Page", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('content', models.TextField()), - ('previous_page', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.Page')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ("content", models.TextField()), + ( + "previous_page", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="testapp.Page", + ), + ), ], ), migrations.CreateModel( - name='Cover', + name="Cover", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('comment', models.CharField(max_length=100)), - ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='testapp.Post')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("comment", models.CharField(max_length=100)), + ( + "post", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="testapp.Post" + ), + ), ], ), migrations.CreateModel( - name='C', + name="C", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('a', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.A')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ( + "a", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="testapp.A", + ), + ), ], ), migrations.CreateModel( - name='B', + name="B", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('c', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.C')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ( + "c", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="testapp.C", + ), + ), ], ), migrations.CreateModel( - name='Account', + name="Account", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(choices=[('c', 'Checking'), ('s', 'Savings')], max_length=1)), - ('name', models.CharField(max_length=80)), - ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='testapp.Customer', to_field='ssn')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "type", + models.CharField( + choices=[("c", "Checking"), ("s", "Savings")], max_length=1 + ), + ), + ("name", models.CharField(max_length=80)), + ( + "customer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="testapp.Customer", + to_field="ssn", + ), + ), ], ), migrations.AddField( - model_name='a', - name='b', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.B'), + model_name="a", + name="b", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, to="testapp.B" + ), ), ] diff --git a/tests/testapp/models.py b/tests/testapp/models.py index 02258a8..0865243 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -6,11 +6,13 @@ class PostQuerySet(models.QuerySet): def annotate_is_published(self): - return self.annotate(is_published=Case( - When(publish_date__isnull=False, then=Value(True)), - default=Value(False), - output_field=models.BooleanField(), - )) + return self.annotate( + is_published=Case( + When(publish_date__isnull=False, then=Value(True)), + default=Value(False), + output_field=models.BooleanField(), + ) + ) class Note(models.Model): @@ -48,27 +50,27 @@ class Cover(models.Model): class Page(models.Model): title = models.CharField(max_length=100) content = models.TextField() - previous_page = models.ForeignKey('self', null=True, on_delete=models.CASCADE) + previous_page = models.ForeignKey("self", null=True, on_delete=models.CASCADE) class A(models.Model): title = models.CharField(max_length=100) - b = models.ForeignKey('B', null=True, on_delete=models.CASCADE) + b = models.ForeignKey("B", null=True, on_delete=models.CASCADE) class B(models.Model): name = models.CharField(max_length=100) - c = models.ForeignKey('C', null=True, on_delete=models.CASCADE) + c = models.ForeignKey("C", null=True, on_delete=models.CASCADE) class C(models.Model): title = models.CharField(max_length=100) - a = models.ForeignKey('A', null=True, on_delete=models.CASCADE) + a = models.ForeignKey("A", null=True, on_delete=models.CASCADE) class Person(models.Model): name = models.CharField(max_length=100) - best_friend = models.ForeignKey('self', null=True, on_delete=models.CASCADE) + best_friend = models.ForeignKey("self", null=True, on_delete=models.CASCADE) date_joined = models.DateField(auto_now_add=True) time_joined = models.TimeField(auto_now_add=True) @@ -84,9 +86,9 @@ class Customer(models.Model): class Account(models.Model): TYPE_CHOICES = [ - ('c', 'Checking'), - ('s', 'Savings'), + ("c", "Checking"), + ("s", "Savings"), ] - customer = models.ForeignKey(Customer, to_field='ssn', on_delete=models.CASCADE) + customer = models.ForeignKey(Customer, to_field="ssn", on_delete=models.CASCADE) type = models.CharField(max_length=1, choices=TYPE_CHOICES) name = models.CharField(max_length=80) diff --git a/tests/testapp/serializers.py b/tests/testapp/serializers.py index 6ad083b..a04dc37 100644 --- a/tests/testapp/serializers.py +++ b/tests/testapp/serializers.py @@ -1,4 +1,3 @@ - from rest_framework import serializers from .models import Note, User @@ -7,10 +6,10 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['pk', 'username', 'email', 'is_staff'] + fields = ["pk", "username", "email", "is_staff"] class NoteSerializer(serializers.ModelSerializer): class Meta: model = Note - fields = ['pk', 'title', 'content', 'author'] + fields = ["pk", "title", "content", "author"] diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py index 8bc29d9..4afa534 100644 --- a/tests/testapp/urls.py +++ b/tests/testapp/urls.py @@ -1,19 +1,18 @@ - from django.urls import include, path from rest_framework import routers from . import views router = routers.DefaultRouter() -router.register('df-users', views.DFUserViewSet, basename='df-users') -router.register('ff-users', views.FilterFieldsUserViewSet, basename='ff-users') -router.register('ffcomplex-users', - views.ComplexFilterFieldsUserViewSet, - basename='ffcomplex-users') -router.register('users', views.UserViewSet) -router.register('notes', views.NoteViewSet) +router.register("df-users", views.DFUserViewSet, basename="df-users") +router.register("ff-users", views.FilterFieldsUserViewSet, basename="ff-users") +router.register( + "ffcomplex-users", views.ComplexFilterFieldsUserViewSet, basename="ffcomplex-users" +) +router.register("users", views.UserViewSet) +router.register("notes", views.NoteViewSet) urlpatterns = [ - path('', include(router.urls)), + path("", include(router.urls)), ] diff --git a/tests/testapp/views.py b/tests/testapp/views.py index 1d9e0e2..e265868 100644 --- a/tests/testapp/views.py +++ b/tests/testapp/views.py @@ -1,4 +1,3 @@ - from rest_framework import pagination, viewsets from rest_framework_filters import backends @@ -29,7 +28,7 @@ class FilterFieldsUserViewSet(viewsets.ModelViewSet): serializer_class = UserSerializer filter_backends = [backends.RestFrameworkFilterBackend] filterset_fields = { - 'username': '__all__', + "username": "__all__", } @@ -40,16 +39,16 @@ class UnfilteredUserViewSet(viewsets.ModelViewSet): class ComplexFilterFieldsUserViewSet(FilterFieldsUserViewSet): - queryset = User.objects.order_by('pk') - filter_backends = (backends.ComplexFilterBackend, ) + queryset = User.objects.order_by("pk") + filter_backends = (backends.ComplexFilterBackend,) filterset_fields = { - 'id': '__all__', - 'username': '__all__', - 'email': '__all__', + "id": "__all__", + "username": "__all__", + "email": "__all__", } class pagination_class(pagination.PageNumberPagination): - page_size_query_param = 'page_size' + page_size_query_param = "page_size" class UserViewSet(viewsets.ModelViewSet): From d19466782f330b247cd93f35e12a749dc7d2dab6 Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Fri, 7 Jun 2024 01:24:12 -0400 Subject: [PATCH 09/10] fix: package name --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9baa145..0808803 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "django-rest-framework-filters" +name = "djangorestframework-filters" version = "1.0.0" homepage = "https://github.com/sdelements/django-rest-framework-filters" description = "Makes it easy to filter across relationships" From 5e01ca2a0657f34c5556327c0c7c7dfe779571b1 Mon Sep 17 00:00:00 2001 From: Sunny Rangnani Date: Fri, 7 Jun 2024 12:22:25 -0400 Subject: [PATCH 10/10] mr comments --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 7241ddf..b579dee 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,13 @@ The interface is straightforward:: set_key_value('foo', 'bar') get_value_for_key('foo') ``` + +## Changelog + +Changes made in this fork: + +- Upgraded to use pyproject.toml +- Use github actions over travis-ci +- Python 3.12 compatibilty +- Django 4.2 compatibilty +- Removed crispy-forms integration