From cfea58f7bbdbf4d220006b4e0d3c1d03d09c7d2f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 13 Jul 2018 12:20:10 +0200 Subject: [PATCH 001/103] Unpin twine Closes #925 --- requirements/maintainer.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/maintainer.txt b/requirements/maintainer.txt index 63f59358a..88a0c1e35 100644 --- a/requirements/maintainer.txt +++ b/requirements/maintainer.txt @@ -23,5 +23,5 @@ Sphinx==1.3.6 sphinx-autobuild==0.6.0 sphinx-rtd-theme==0.1.9 tornado==4.2.1 -twine==1.6.5 +twine watchdog==0.8.3 From 22dd8263be73d4e2e7a6501b1194547ed65a672b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 13 Jul 2018 12:41:56 +0200 Subject: [PATCH 002/103] Correct typo in filter docs Closes #896 --- docs/ref/filters.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index d136c555e..6f33e483f 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -688,7 +688,7 @@ comma-separated values. Example:: - class NumberRangeFilter(BaseInFilter, NumberFilter): + class NumberRangeFilter(BaseRangeFilter, NumberFilter): pass class F(FilterSet): From e924eb7c58eebf6f1eea328f60411701bf667164 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 13 Jul 2018 12:44:37 +0200 Subject: [PATCH 003/103] =?UTF-8?q?Note=20filter=5Ffields=20doesn=E2=80=99?= =?UTF-8?q?t=20work=20with=20filter=5Fclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #905 --- docs/guide/rest_framework.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index 480c819a7..618d3c66b 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -105,6 +105,10 @@ You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to y fields = ('category', 'in_stock') +Note that using ``filter_fields`` and ``filter_class`` together is not +supported. + + Overriding FilterSet creation ----------------------------- From 0964806b23bb0e27d31422e1ed6c02e5d499ea4d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 13 Jul 2018 21:08:25 +0200 Subject: [PATCH 004/103] Update name to field_name in docs. Ref #945. --- docs/ref/filters.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 6f33e483f..6af76fdfd 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -11,8 +11,8 @@ Core Arguments The following are the core arguments that apply to all filters. -``name`` -~~~~~~~~ +``field_name`` +~~~~~~~~~~~~~~ The name of the field this filter is supposed to filter on, if this is not provided it automatically becomes the filter's name on the ``FilterSet``. From 5a7f14fdb0b610dd2a6921f9b91a6c15c1248457 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 13 Jul 2018 21:27:04 +0200 Subject: [PATCH 005/103] More updates of name to field_name in docs. Ref #945. --- docs/guide/install.txt | 2 +- docs/guide/rest_framework.txt | 4 ++-- docs/guide/usage.txt | 17 ++++++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/guide/install.txt b/docs/guide/install.txt index 9a6c09467..edcc520cd 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -31,4 +31,4 @@ __ http://www.django-rest-framework.org/ * **Python**: 3.4, 3.5, 3.6, 3.7 * **Django**: 1.11, 2.0 -* **DRF**: 3.7 +* **DRF**: 3.8 diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index 618d3c66b..bf729e01f 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -64,8 +64,8 @@ To enable filtering with a ``FilterSet``, add it to the ``filter_class`` paramet class ProductFilter(filters.FilterSet): - min_price = filters.NumberFilter(name="price", lookup_expr='gte') - max_price = filters.NumberFilter(name="price", lookup_expr='lte') + min_price = filters.NumberFilter(field_name="price", lookup_expr='gte') + max_price = filters.NumberFilter(field_name="price", lookup_expr='lte') class Meta: model = Product diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 1fc1c01f3..4ee40a66a 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -54,12 +54,12 @@ the :ref:`core filter arguments ` on a ``FilterSet``:: class ProductFilter(django_filters.FilterSet): price = django_filters.NumberFilter() - price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt') - price__lt = django_filters.NumberFilter(name='price', lookup_expr='lt') + price__gt = django_filters.NumberFilter(field_name='price', lookup_expr='gt') + price__lt = django_filters.NumberFilter(field_name='price', lookup_expr='lt') - release_year = django_filters.NumberFilter(name='release_date', lookup_expr='year') - release_year__gt = django_filters.NumberFilter(name='release_date', lookup_expr='year__gt') - release_year__lt = django_filters.NumberFilter(name='release_date', lookup_expr='year__lt') + release_year = django_filters.NumberFilter(field_name='release_date', lookup_expr='year') + release_year__gt = django_filters.NumberFilter(field_name='release_date', lookup_expr='year__gt') + release_year__lt = django_filters.NumberFilter(field_name='release_date', lookup_expr='year__lt') manufacturer__name = django_filters.CharFilter(lookup_expr='icontains') @@ -68,7 +68,7 @@ the :ref:`core filter arguments ` on a ``FilterSet``:: There are two main arguments for filters: -- ``name``: The name of the model field to filter on. You can traverse +- ``field_name``: The name of the model field to filter on. You can traverse "relationship paths" using Django's ``__`` syntax to filter fields on a related model. ex, ``manufacturer__name``. - ``lookup_expr``: The `field lookup`_ to use when filtering. Django's ``__`` @@ -77,11 +77,10 @@ There are two main arguments for filters: .. _`field lookup`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups -Together, the field ``name`` and ``lookup_expr`` represent a complete Django +Together, the field ``field_name`` and ``lookup_expr`` represent a complete Django lookup expression. A detailed explanation of lookup expressions is provided in Django's `lookup reference`_. django-filter supports expressions containing -both transforms and a final lookup for version 1.9 of Django and above. -For Django version 1.8, transformed expressions are not supported. +both transforms and a final lookup. .. _`lookup reference`: https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups From a0635c0cac464f6b43a7eff6b91d73b2d59fc675 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Sat, 14 Jul 2018 02:11:46 -0400 Subject: [PATCH 006/103] Update docs (#946) * Add 'Keyword-only Arguments' section * Update docs for core arguments * Nest field-related arguments under **kwargs * Update docs for kw-only arguments * Update name => field_name in docs * Drop old setting from docs * Update view attribute names in docs --- docs/guide/rest_framework.txt | 25 +++--- docs/guide/tips.txt | 24 +++--- docs/guide/usage.txt | 2 +- docs/ref/filters.txt | 154 +++++++++++++++------------------- 4 files changed, 94 insertions(+), 111 deletions(-) diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index bf729e01f..4a21a566d 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -30,7 +30,7 @@ Your view class will also need to add ``DjangoFilterBackend`` to the ``filter_ba queryset = Product.objects.all() serializer_class = ProductSerializer filter_backends = (filters.DjangoFilterBackend,) - filter_fields = ('category', 'in_stock') + filterset_fields = ('category', 'in_stock') If you want to use the django-filter backend by default, add it to the ``DEFAULT_FILTER_BACKENDS`` setting. @@ -51,10 +51,10 @@ If you want to use the django-filter backend by default, add it to the ``DEFAULT } -Adding a FilterSet with ``filter_class`` ----------------------------------------- +Adding a FilterSet with ``filterset_class`` +------------------------------------------- -To enable filtering with a ``FilterSet``, add it to the ``filter_class`` parameter on your view class. +To enable filtering with a ``FilterSet``, add it to the ``filterset_class`` parameter on your view class. .. code-block:: python @@ -76,13 +76,13 @@ To enable filtering with a ``FilterSet``, add it to the ``filter_class`` paramet queryset = Product.objects.all() serializer_class = ProductSerializer filter_backends = (filters.DjangoFilterBackend,) - filter_class = ProductFilter + filterset_class = ProductFilter -Using the ``filter_fields`` shortcut ------------------------------------- +Using the ``filterset_fields`` shortcut +--------------------------------------- -You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to your view class. This is equivalent to creating a FilterSet with just :ref:`Meta.fields `. +You may bypass creating a ``FilterSet`` by instead adding ``filterset_fields`` to your view class. This is equivalent to creating a FilterSet with just :ref:`Meta.fields `. .. code-block:: python @@ -95,7 +95,7 @@ You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to y class ProductList(generics.ListAPIView): queryset = Product.objects.all() filter_backends = (filters.DjangoFilterBackend,) - filter_fields = ('category', 'in_stock') + filterset_fields = ('category', 'in_stock') # Equivalent FilterSet: @@ -105,7 +105,7 @@ You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to y fields = ('category', 'in_stock') -Note that using ``filter_fields`` and ``filter_class`` together is not +Note that using ``filterset_fields`` and ``filterset_class`` together is not supported. @@ -141,7 +141,7 @@ You can override these methods on a case-by-case basis for each view, creating u class BookViewSet(viewsets.ModelViewSet): filter_backends = [MyFilterBackend] - filter_class = BookFilter + filterset_class = BookFilter def get_filteset_kwargs(self): return { @@ -228,5 +228,4 @@ The following features are specific to the rest framework FilterSet: - ``BooleanFilter``'s use the API-friendly ``BooleanWidget``, which accepts lowercase ``true``/``false``. - Filter generation uses ``IsoDateTimeFilter`` for datetime model fields. -- Raised ``ValidationError``'s are reraised as their DRF equivalent. This behavior is useful when setting FilterSet - strictness to ``STRICTNESS.RAISE_VALIDATION_ERROR``. +- Raised ``ValidationError``'s are reraised as their DRF equivalent. diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index a383ba199..76563028e 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -10,11 +10,11 @@ recommended that you read this as it provides a more complete understanding of how filters work. -Filter ``name`` and ``lookup_expr`` not configured -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Filter ``field_name`` and ``lookup_expr`` not configured +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -While ``name`` and ``lookup_expr`` are optional, it is recommended that you specify -them. By default, if ``name`` is not specified, the filter's name on the +While ``field_name`` and ``lookup_expr`` are optional, it is recommended that you specify +them. By default, if ``field_name`` is not specified, the filter's name on the filterset class will be used. Additionally, ``lookup_expr`` defaults to ``exact``. The following is an example of a misconfigured price filter: @@ -36,7 +36,7 @@ would be: .. code-block:: python class ProductFilter(django_filters.FilterSet): - price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt') + price__gt = django_filters.NumberFilter(field_name='price', lookup_expr='gt') Missing ``lookup_expr`` for text search filters @@ -68,7 +68,7 @@ for uncategorized products. The following is an incorrectly configured .. code-block:: python class ProductFilter(django_filters.FilterSet): - uncategorized = django_filters.NumberFilter(name='category', lookup_expr='isnull') + uncategorized = django_filters.NumberFilter(field_name='category', lookup_expr='isnull') So what's the issue? While the underlying column type for ``category`` is an integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however @@ -84,8 +84,8 @@ uncategorized products and products for a set of categories: pass class ProductFilter(django_filters.FilterSet): - categories = NumberInFilter(name='category', lookup_expr='in') - uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull') + categories = NumberInFilter(field_name='category', lookup_expr='in') + uncategorized = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull') More info on constructing ``in`` and ``range`` csv :ref:`filters `. @@ -112,7 +112,7 @@ the FilterSet's automatic filter generation. To do this manually, simply add: .. code-block:: python class ProductFilter(django_filters.FilterSet): - uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull') + uncategorized = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull') .. note:: @@ -124,7 +124,7 @@ You may also reverse the logic with the ``exclude`` parameter. .. code-block:: python class ProductFilter(django_filters.FilterSet): - has_category = django_filters.BooleanFilter(name='category', lookup_expr='isnull', exclude=True) + has_category = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull', exclude=True) Solution 2: Using ``ChoiceFilter``'s null choice """""""""""""""""""""""""""""""""""""""""""""""" @@ -137,7 +137,7 @@ the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference class ProductFilter(django_filters.FilterSet): category = django_filters.ModelChoiceFilter( - name='category', lookup_expr='isnull', + field_name='category', lookup_expr='isnull', null_label='Uncategorized', queryset=Category.objects.all(), ) @@ -203,7 +203,7 @@ behavior as an ``isnull`` filter. class MyFilterSet(filters.FilterSet): - myfield__isempty = EmptyStringFilter(name='myfield') + myfield__isempty = EmptyStringFilter(field_name='myfield') class Meta: model = MyModel diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 4ee40a66a..7b9470b6f 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -311,7 +311,7 @@ You must provide either a ``model`` or ``filterset_class`` argument, similar to url(r'^list/$', FilterView.as_view(model=Product)), ] -If you provide a ``model`` optionally you can set ``filter_fields`` to specify a list or a tuple of +If you provide a ``model`` optionally you can set ``filterset_fields`` to specify a list or a tuple of the fields that you want to include for the automatic construction of the filterset class. You must provide a template at ``/_filter.html`` which gets the diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 6af76fdfd..876914e0a 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -9,42 +9,45 @@ This is a reference document with a list of the filters and their arguments. Core Arguments -------------- -The following are the core arguments that apply to all filters. +The following are the core arguments that apply to all filters. Note that they +are joined to construct the complete `lookup expression`_ that is the left hand +side of the ORM ``.filter()`` call. + +.. _`lookup expression`: https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups ``field_name`` ~~~~~~~~~~~~~~ -The name of the field this filter is supposed to filter on, if this is not -provided it automatically becomes the filter's name on the ``FilterSet``. -You can traverse "relationship paths" using Django's ``__`` syntax to filter -fields on a related model. eg, ``manufacturer__name``. +The name of the model field that is filtered against. If this argument is not +provided, it defaults the filter's attribute name on the ``FilterSet`` class. +Field names can traverse relationships by joining the related parts with the ORM +lookup separator (``__``). e.g., a product's ``manufacturer__name``. + +``lookup_expr`` +~~~~~~~~~~~~~~~ + +The `field lookup`_ that should be performed in the filter call. Defaults to +``exact``. The ``lookup_expr`` can contain transforms if the expression parts +are joined by the ORM lookup separator (``__``). e.g., filter a datetime by its +year part ``year__gt``. + +.. _`Field lookup`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups + +.. _keyword-only-arguments: + +Keyword-only Arguments: +----------------------- + +The following are optional arguments that can be used to modify the behavior of +all filters. ``label`` ~~~~~~~~~ The label as it will apear in the HTML, analogous to a form field's label argument. If a label is not provided, a verbose label will be generated based -on the field ``name`` and the parts of the ``lookup_expr``. -(See: :ref:`verbose-lookups-setting`). - -``widget`` -~~~~~~~~~~ - -The django.form Widget class which will represent the ``Filter``. In addition -to the widgets that are included with Django that you can use there are -additional ones that django-filter provides which may be useful: - - * :ref:`LinkWidget ` -- this displays the options in a manner - similar to the way the Django Admin does, as a series of links. The link - for the selected option will have ``class="selected"``. - * :ref:`BooleanWidget ` -- this widget converts its input - into Python's True/False values. It will convert all case variations of - ``True`` and ``False`` into the internal Python values. - * :ref:`CSVWidget ` -- this widget expects a comma separated - value and converts it into a list of string values. It is expected that - the field class handle a list of values as well as type conversion. - * :ref:`RangeWidget ` -- this widget is used with ``RangeFilter`` - to generate two form input elements using a single field. +on the field ``field_name`` and the parts of the ``lookup_expr`` +(see: :ref:`verbose-lookups-setting`). .. _filter-method: @@ -53,18 +56,17 @@ additional ones that django-filter provides which may be useful: An optional argument that tells the filter how to handle the queryset. It can accept either a callable or the name of a method on the ``FilterSet``. The -method receives a ``QuerySet``, the name of the model field to filter on, and -the value to filter with. It should return a ``Queryset`` that is filtered -appropriately. +callable receives a ``QuerySet``, the name of the model field to filter on, and +the value to filter with. It should return a filtered ``Queryset``. -The passed in value is validated and cleaned by the filter's ``field_class``, -so raw value transformation and empty value checking should be unnecessary. +Note that the value is validated by the ``Filter.field``, so raw value +transformation and empty value checking should be unnecessary. .. code-block:: python class F(FilterSet): """Filter for Books by if books are published or not""" - published = BooleanFilter(name='published_on', method='filter_published') + published = BooleanFilter(field_name='published_on', method='filter_published') def filter_published(self, queryset, name, value): # construct the full lookup expression. @@ -86,70 +88,52 @@ so raw value transformation and empty value checking should be unnecessary. class F(FilterSet): """Filter for Books by if books are published or not""" - published = BooleanFilter(name='published_on', method=filter_not_empty) + published = BooleanFilter(field_name='published_on', method=filter_not_empty) class Meta: model = Book fields = ['published'] -``lookup_expr`` -~~~~~~~~~~~~~~~ - -The lookup expression that should be performed using `Django's ORM`_. - -.. _`Django's ORM`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups - -A ``list`` or ``tuple`` of lookup types is also accepted, allowing the user to -select the lookup from a dropdown. The list of lookup types are filtered against -``filters.LOOKUP_TYPES``. If `lookup_expr=None` is passed, then a list of all lookup -types will be generated:: - - class ProductFilter(django_filters.FilterSet): - name = django_filters.CharFilter(lookup_expr=['exact', 'iexact']) - -You can enable custom lookups by adding them to ``LOOKUP_TYPES``:: - - from django_filters import filters - - filters.LOOKUP_TYPES = ['gt', 'gte', 'lt', 'lte', 'custom_lookup_type'] - -Additionally, you can provide human-friendly help text by overriding ``LOOKUP_TYPES``:: - - # filters.py - from django_filters import filters - - filters.LOOKUP_TYPES = [ - ('', '---------'), - ('exact', 'Is equal to'), - ('not_exact', 'Is not equal to'), - ('lt', 'Lesser than'), - ('gt', 'Greater than'), - ('gte', 'Greater than or equal to'), - ('lte', 'Lesser than or equal to'), - ('startswith', 'Starts with'), - ('endswith', 'Ends with'), - ('contains', 'Contains'), - ('not_contains', 'Does not contain'), - ] - ``distinct`` ~~~~~~~~~~~~ -A boolean value that specifies whether the Filter will use distinct on the -queryset. This option can be used to eliminate duplicate results when using filters that span related models. Defaults to ``False``. +A boolean that specifies whether the Filter will use distinct on the queryset. +This option can be used to eliminate duplicate results when using filters that +span relationships. Defaults to ``False``. ``exclude`` ~~~~~~~~~~~ -A boolean value that specifies whether the Filter should use ``filter`` or ``exclude`` on the queryset. -Defaults to ``False``. +A boolean that specifies whether the Filter should use ``filter`` or ``exclude`` +on the queryset. Defaults to ``False``. ``**kwargs`` ~~~~~~~~~~~~ -Any additional keyword arguments are stored as the ``extra`` parameter on the filter. They are provided to the accompanying form Field and can be used to provide arguments like ``choices``. +Any additional keyword arguments are stored as the ``extra`` parameter on the +filter. They are provided to the accompanying form ``Field`` and can be used to +provide arguments like ``choices``. Some field-related arguments: + +``widget`` +"""""""""" + +The django.form Widget class which will represent the ``Filter``. In addition +to the widgets that are included with Django that you can use there are +additional ones that django-filter provides which may be useful: + + * :ref:`LinkWidget ` -- this displays the options in a manner + similar to the way the Django Admin does, as a series of links. The link + for the selected option will have ``class="selected"``. + * :ref:`BooleanWidget ` -- this widget converts its input + into Python's True/False values. It will convert all case variations of + ``True`` and ``False`` into the internal Python values. + * :ref:`CSVWidget ` -- this widget expects a comma separated + value and converts it into a list of string values. It is expected that + the field class handle a list of values as well as type conversion. + * :ref:`RangeWidget ` -- this widget is used with ``RangeFilter`` + to generate two form input elements using a single field. ModelChoiceFilter and ModelMultipleChoiceFilter arguments @@ -389,7 +373,7 @@ To use a custom field name for the lookup, you can use ``to_field_name``:: class FooFilter(BaseFilterSet): foo = django_filters.filters.ModelMultipleChoiceFilter( - name='attr__uuid', + field_name='attr__uuid', to_field_name='uuid', queryset=Foo.objects.all(), ) @@ -665,7 +649,7 @@ Example:: pass class F(FilterSet): - id__in = NumberInFilter(name='id', lookup_expr='in') + id__in = NumberInFilter(field_name='id', lookup_expr='in') class Meta: model = User @@ -692,7 +676,7 @@ Example:: pass class F(FilterSet): - id__range = NumberRangeFilter(name='id', lookup_expr='range') + id__range = NumberRangeFilter(field_name='id', lookup_expr='range') class Meta: model = User @@ -730,8 +714,8 @@ two additional arguments that are used to build the ordering choices. .. code-block:: python class UserFilter(FilterSet): - account = CharFilter(name='username') - status = NumberFilter(name='status') + account = CharFilter(field_name='username') + status = NumberFilter(field_name='status') o = OrderingFilter( # tuple-mapping retains order @@ -768,8 +752,8 @@ want to disable descending sort options. .. code-block:: python class UserFilter(FilterSet): - account = CharFilter(name='username') - status = NumberFilter(name='status') + account = CharFilter(field_name='username') + status = NumberFilter(field_name='status') o = OrderingFilter( choices=( From 5341a5c3b610c9a9e98a7f7f485fd5910c30942b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 19 Jul 2018 09:12:12 +0200 Subject: [PATCH 007/103] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..3ac74031e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,7 @@ +--- +name: Bug report +about: Updating to 2.0? Did you read the migration guide? https://django-filter.readthedocs.io/en/master/guide/migration.html#migration-guide + +--- + + From 91b6dd49bed885432f9590ab8396382790015879 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Sun, 22 Jul 2018 23:54:52 -0700 Subject: [PATCH 008/103] Add 'python_requires' check (#953) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e694cae45..56d003a2e 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ 'Framework :: Django', ], zip_safe=False, + python_requires='>=3.4', install_requires=[ 'Django>=1.11', ], From 1dde11c70eedeac32f6de92c426deb289e1aebd8 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 2 Aug 2018 21:27:30 +0200 Subject: [PATCH 009/103] Add Django 2.1 to --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7adc58c17..99aa8e073 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ Requirements ------------ * **Python**: 3.4, 3.5, 3.6, 3.7 -* **Django**: 1.11, 2.0 +* **Django**: 1.11, 2.0, 2.1 * **DRF**: 3.8+ From Version 2.0 Django Filter is Python 3 only. From 8ca7e0c78550813d6d60a683840e9361d5c4610f Mon Sep 17 00:00:00 2001 From: Matt Wiens Date: Thu, 9 Aug 2018 22:54:08 -0700 Subject: [PATCH 010/103] Fix spelling mistakes in docs (#962) --- docs/guide/migration.txt | 4 ++-- docs/ref/filters.txt | 4 ++-- docs/ref/widgets.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/migration.txt b/docs/guide/migration.txt index 5fefe67b7..3b75ea082 100644 --- a/docs/guide/migration.txt +++ b/docs/guide/migration.txt @@ -33,7 +33,7 @@ Migrating to 2.0 This release contains several changes that break forwards compatibility. This includes removed features, renamed attributes and arguments, and some reworked features. Due to the nature of these changes, it is not feasible to release -a fully forards-compatible migration release. Please review the below list of +a fully forwards-compatible migration release. Please review the below list of changes and update your code accordingly. @@ -74,7 +74,7 @@ __ https://github.com/carltongibson/django-filter/pull/791 The ``Meta.together`` has been deprecated in favor of userland implementations that override the ``clean`` method of the ``Meta.form`` class. An example will -be provided in a "recipes" secion in future docs. +be provided in a "recipes" section in future docs. FilterSet "strictness" handling moved to view (`#788`__) diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 876914e0a..986bbeeaa 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -44,7 +44,7 @@ all filters. ``label`` ~~~~~~~~~ -The label as it will apear in the HTML, analogous to a form field's label +The label as it will appear in the HTML, analogous to a form field's label argument. If a label is not provided, a verbose label will be generated based on the field ``field_name`` and the parts of the ``lookup_expr`` (see: :ref:`verbose-lookups-setting`). @@ -616,7 +616,7 @@ of the admin. A combined filter that allows users to select the lookup expression from a dropdown. * ``lookup_choices`` is an optional argument that accepts multiple input - formats, and is ultimately normlized as the choices used in the lookup + formats, and is ultimately normalized as the choices used in the lookup dropdown. See ``.get_lookup_choices()`` for more information. * ``field_class`` is an optional argument that allows you to set the inner diff --git a/docs/ref/widgets.txt b/docs/ref/widgets.txt index b5c5e7d0b..b9b34d14f 100644 --- a/docs/ref/widgets.txt +++ b/docs/ref/widgets.txt @@ -54,7 +54,7 @@ well as type conversion. This widget is used with ``RangeFilter`` and its subclasses. It generates two form input elements which generally act as start/end values in a range. -Under the hood, it is django's ``forms.TextInput`` widget and excepts +Under the hood, it is Django's ``forms.TextInput`` widget and excepts the same arguments and values. To use it, pass it to ``widget`` argument of a ``RangeField``: From 90acc94022956563355567a8ba340146c63c73e8 Mon Sep 17 00:00:00 2001 From: Adler Medrado Date: Thu, 6 Sep 2018 02:34:53 -0300 Subject: [PATCH 011/103] Fix typo on dev/guide/rest_framework (#972) --- docs/guide/rest_framework.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index 4a21a566d..12b2c884e 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -133,7 +133,7 @@ You can override these methods on a case-by-case basis for each view, creating u return kwargs - class BooksFilter(filters.FitlerSet): + class BooksFilter(filters.FilterSet): def __init__(self, *args, author=None, **kwargs): super().__init__(*args, **kwargs) # do something w/ author From eef406c1ee429e3024999c8746d1ca5641185bf8 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 12 Sep 2018 08:05:29 +0000 Subject: [PATCH 012/103] Document Django 2.1 support everywhere and update test matrix (#974) * Document Django 2.1 support everywhere * Run tests against DRF 3.8+ Documentation says that DRF 3.8+ is required --- docs/guide/install.txt | 4 ++-- setup.py | 1 + tox.ini | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/guide/install.txt b/docs/guide/install.txt index edcc520cd..503f830b6 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -30,5 +30,5 @@ __ http://www.django-rest-framework.org/ * **Python**: 3.4, 3.5, 3.6, 3.7 -* **Django**: 1.11, 2.0 -* **DRF**: 3.8 +* **Django**: 1.11, 2.0, 2.1 +* **DRF**: 3.8+ diff --git a/setup.py b/setup.py index 56d003a2e..1c1e0f127 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ 'Framework :: Django', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', + 'Framework :: Django :: 2.1', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', diff --git a/tox.ini b/tox.ini index 7f63bdd4f..fe45064e6 100644 --- a/tox.ini +++ b/tox.ini @@ -21,8 +21,8 @@ setenv = deps = django111: django>=1.11.0,<2.0 django20: django>=2.0,<2.1 - django21: django>=2.1b1 - djangorestframework>=3.7,<3.8 + django21: django>=2.1,<2.2 + djangorestframework>=3.8,<3.9 latest: {[latest]deps} -rrequirements/test-ci.txt From a4112ad4b2427b0fc03ef6adf7dc09a3d9c67743 Mon Sep 17 00:00:00 2001 From: John Carter Date: Tue, 30 Oct 2018 03:04:42 +1300 Subject: [PATCH 013/103] pin detox/tox workaround compatiblity issue (#991) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f59e22fa9..7f93f9a1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,8 @@ python: cache: pip install: - - pip install coverage detox tox-travis tox-venv + # latest detox and tox aren't compatible https://github.com/carltongibson/django-filter/issues/990 + - pip install coverage detox==0.13 tox~=3.2.1 tox-travis tox-venv script: - coverage erase From 2ceaaaba806a3069ba5e7c3b40c2ac3d2a5c45a9 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 30 Oct 2018 11:25:16 +0100 Subject: [PATCH 014/103] Update requests dependency. (#993) Remove (unused) requests-toolbelt. --- requirements/maintainer.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements/maintainer.txt b/requirements/maintainer.txt index 88a0c1e35..e2b43a9d8 100644 --- a/requirements/maintainer.txt +++ b/requirements/maintainer.txt @@ -15,8 +15,7 @@ pkginfo==1.2.1 Pygments==2.1.3 pytz==2016.6.1 PyYAML==3.11 -requests==2.9.1 -requests-toolbelt==0.6.0 +requests==2.20.0 six==1.9.0 snowballstemmer==1.2.1 Sphinx==1.3.6 From b813813b3d4f370de89c430b024342c03d0e4982 Mon Sep 17 00:00:00 2001 From: Matt Cooper Date: Mon, 5 Nov 2018 04:57:23 -0500 Subject: [PATCH 015/103] Added Azure Pipelines configuration (#998) --- .azure_pipelines/azure-pipelines.yml | 69 ++++++++++++++++++++++++++++ requirements/test-ci.txt | 1 + tox.ini | 4 +- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 .azure_pipelines/azure-pipelines.yml diff --git a/.azure_pipelines/azure-pipelines.yml b/.azure_pipelines/azure-pipelines.yml new file mode 100644 index 000000000..c58e9a58b --- /dev/null +++ b/.azure_pipelines/azure-pipelines.yml @@ -0,0 +1,69 @@ +#################### +# Azure Pipelines +#################### + +strategy: + matrix: + # For some reason, Python 3.4 tries to install Django 2.0 - disabling for now + # PY34: + # PYTHON_VERSION: '3.4' + PY35: + PYTHON_VERSION: '3.5' + PY36: + PYTHON_VERSION: '3.6' + PY36_isort: + PYTHON_VERSION: '3.6' + TOXENV: isort,lint,docs + PY36_warnings: + PYTHON_VERSION: '3.6' + TOXENV: warnings + PY37: + PYTHON_VERSION: '3.7' + +steps: +- task: UsePythonVersion@0 + inputs: + versionSpec: '$(PYTHON_VERSION)' + architecture: 'x64' + +- script: | + python -m pip install --upgrade pip setuptools + displayName: Ensure latest setuptools + +- script: | + pip install coverage tox tox-venv unittest-xml-reporting + displayName: Install deps + +- script: | + pip --version + tox --skip-missing-interpreters true + displayName: Run tox + +- script: | + coverage combine + coverage report -m + coverage xml + coverage html + displayName: Coverage reporting + condition: eq(variables['TOXENV'], '') # skip for the custom TOXENV legs + +- task: PublishCodeCoverageResults@1 + displayName: Publish coverage results + inputs: + codeCoverageTool: cobertura + summaryFileLocation: '$(Build.SourcesDirectory)\coverage.xml' + reportDirectory: '$(Build.SourcesDirectory)\htmlcov' + condition: eq(variables['TOXENV'], '') # skip for the custom TOXENV legs + +- task: PublishTestResults@2 + displayName: Publish test results + inputs: + testResultsFiles: '**/TEST-*.xml' + testRunTitle: Python $(PYTHON_VERSION) + +# codecov disabled for now +- script: | + pip install codecov + codecov + displayName: Codecov + condition: false \ No newline at end of file diff --git a/requirements/test-ci.txt b/requirements/test-ci.txt index 97254f996..d64ba8988 100644 --- a/requirements/test-ci.txt +++ b/requirements/test-ci.txt @@ -5,3 +5,4 @@ django-crispy-forms coverage mock pytz +unittest-xml-reporting diff --git a/tox.ini b/tox.ini index fe45064e6..ffcbb0031 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ deps = https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz [testenv] -commands = coverage run --parallel-mode --source django_filters ./runtests.py {posargs} +commands = coverage run --parallel-mode --source django_filters ./runtests.py --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner {posargs} ignore_outcome = latest: True setenv = @@ -43,7 +43,7 @@ deps = [testenv:warnings] ignore_outcome = True unignore_outcomes = True -commands = python -Werror ./runtests.py {posargs} +commands = python -Werror ./runtests.py --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner {posargs} deps = {[latest]deps} -rrequirements/test-ci.txt From cd738f5a5affcdb900e3cd97191faa41b8e00ac4 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 23 Nov 2018 03:27:29 -0800 Subject: [PATCH 016/103] Update Travis Python 3.7 build (#997) * Python version numbers should be strings * Add updated Python 3.7 build * Travis released Xenial support * Update utility builds to Python 3.7 * Enable 'fast_finish' for Travis --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7f93f9a1d..61c2c8cf9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ sudo: false +dist: xenial language: python python: - "3.4" - "3.5" - "3.6" - - "3.7-dev" + - "3.7" cache: pip @@ -18,10 +19,11 @@ script: - detox matrix: + fast_finish: true include: - - python: 3.6 + - python: "3.7" env: TOXENV=isort,lint,docs - - python: 3.6 + - python: "3.7" env: TOXENV=warnings allow_failures: - env: TOXENV=warnings From ad0ad45a0d052c01ad135009bacd0dcb8492d3aa Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 29 Nov 2018 15:51:40 -0800 Subject: [PATCH 017/103] Add azure-pipelines badge. (#1011) --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 99aa8e073..2f86b729f 100644 --- a/README.rst +++ b/README.rst @@ -6,6 +6,9 @@ add dynamic ``QuerySet`` filtering from URL parameters. Full documentation on `read the docs`_. +.. image:: https://dev.azure.com/noumenal/Django%20Filter/_apis/build/status/Django%20Filter-CI + :target:https://dev.azure.com/noumenal/Django%20Filter/_build/latest?definitionId=3 + .. image:: https://travis-ci.org/carltongibson/django-filter.svg?branch=master :target: https://travis-ci.org/carltongibson/django-filter From 15683cd14e0f20a0bed8abab3448b5789e78fc62 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 29 Nov 2018 18:58:03 -0800 Subject: [PATCH 018/103] Correct badge markup --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2f86b729f..57597f043 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ add dynamic ``QuerySet`` filtering from URL parameters. Full documentation on `read the docs`_. .. image:: https://dev.azure.com/noumenal/Django%20Filter/_apis/build/status/Django%20Filter-CI - :target:https://dev.azure.com/noumenal/Django%20Filter/_build/latest?definitionId=3 + :target: https://dev.azure.com/noumenal/Django%20Filter/_build/latest?definitionId=3 .. image:: https://travis-ci.org/carltongibson/django-filter.svg?branch=master :target: https://travis-ci.org/carltongibson/django-filter @@ -18,7 +18,6 @@ Full documentation on `read the docs`_. .. image:: https://badge.fury.io/py/django-filter.svg :target: http://badge.fury.io/py/django-filter - Requirements ------------ From a6ffe8b1b943ff5663c2c7eb9bacf92db497c689 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 17 Dec 2018 18:34:14 +0100 Subject: [PATCH 019/103] Fixed typo in the docs/guide/rest_framework.txt. (#1016) --- docs/guide/rest_framework.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index 12b2c884e..69e336edd 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -143,7 +143,7 @@ You can override these methods on a case-by-case basis for each view, creating u filter_backends = [MyFilterBackend] filterset_class = BookFilter - def get_filteset_kwargs(self): + def get_filterset_kwargs(self): return { 'author': self.get_author(), } From b3c0b8189689571c357bb89bc11977cbbca4271b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 17 Dec 2018 18:39:02 +0100 Subject: [PATCH 020/103] Fix translate_validation: use params (#965) --- django_filters/utils.py | 3 ++- tests/test_utils.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/django_filters/utils.py b/django_filters/utils.py index 781d3f436..dcd854ece 100644 --- a/django_filters/utils.py +++ b/django_filters/utils.py @@ -317,7 +317,8 @@ def translate_validation(error_dict): from rest_framework.exceptions import ValidationError, ErrorDetail exc = OrderedDict( - (key, [ErrorDetail(e.message, code=e.code) for e in error_list]) + (key, [ErrorDetail(e.message % (e.params or ()), code=e.code) + for e in error_list]) for key, error_list in error_dict.as_data().items() ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 2d89a9a54..7a57fae46 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,6 +10,7 @@ from django_filters import FilterSet from django_filters.exceptions import FieldLookupError +from django_filters.filters import MultipleChoiceFilter from django_filters.utils import ( MigrationNotice, RenameAttributesBase, @@ -511,17 +512,30 @@ class Meta: model = Article fields = ['id', 'author', 'name'] + choice = MultipleChoiceFilter(choices=[('1', 'one'), ('2', 'two')]) + def test_error_detail(self): - f = self.F(data={'id': 'foo', 'author': 'bar', 'name': 'baz'}) + f = self.F(data={ + 'id': 'foo', + 'author': 'bar', + 'name': 'baz', + 'choice': ['3'], + }) exc = translate_validation(f.errors) self.assertDictEqual(exc.detail, { 'id': ['Enter a number.'], 'author': ['Select a valid choice. That choice is not one of the available choices.'], + 'choice': ['Select a valid choice. 3 is not one of the available choices.'], }) def test_full_error_details(self): - f = self.F(data={'id': 'foo', 'author': 'bar', 'name': 'baz'}) + f = self.F(data={ + 'id': 'foo', + 'author': 'bar', + 'name': 'baz', + 'choice': ['3'], + }) exc = translate_validation(f.errors) self.assertEqual(exc.get_full_details(), { @@ -530,4 +544,8 @@ def test_full_error_details(self): 'message': 'Select a valid choice. That choice is not one of the available choices.', 'code': 'invalid_choice', }], + 'choice': [{ + 'message': 'Select a valid choice. 3 is not one of the available choices.', + 'code': 'invalid_choice', + }], }) From 1f47e36b614724a8735e0457fa511dcaf5448481 Mon Sep 17 00:00:00 2001 From: John Carter Date: Tue, 18 Dec 2018 06:53:10 +1300 Subject: [PATCH 021/103] fix DeprecationWarning on python 3.7 (#989) --- django_filters/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_filters/widgets.py b/django_filters/widgets.py index fc7490299..756f4f776 100644 --- a/django_filters/widgets.py +++ b/django_filters/widgets.py @@ -1,4 +1,4 @@ -from collections import Iterable +from collections.abc import Iterable from itertools import chain from re import search, sub From f4981c8644826fc343efcec601bbb91eeb390162 Mon Sep 17 00:00:00 2001 From: Joris Beckers Date: Mon, 17 Dec 2018 21:08:02 +0100 Subject: [PATCH 022/103] Add IsoDateTimeFromToRangeFilter (#1004) (#1005) * Add IsoDateTimeFromToRangeFilter (#1004) * Extra tests with ISO dates * Add documentation for `IsoDateTimeFromToRangeFilter` --- django_filters/fields.py | 10 ++++++++ django_filters/filters.py | 8 ++++++- docs/ref/filters.txt | 33 +++++++++++++++++++++++++++ tests/test_fields.py | 17 ++++++++++++++ tests/test_filtering.py | 28 +++++++++++++++++++++++ tests/test_filters.py | 48 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 1 deletion(-) diff --git a/django_filters/fields.py b/django_filters/fields.py index 9dc5bfabf..617055e4d 100644 --- a/django_filters/fields.py +++ b/django_filters/fields.py @@ -70,6 +70,16 @@ def __init__(self, *args, **kwargs): super().__init__(fields, *args, **kwargs) +class IsoDateTimeRangeField(RangeField): + widget = DateRangeWidget + + def __init__(self, *args, **kwargs): + fields = ( + IsoDateTimeField(), + IsoDateTimeField()) + super().__init__(fields, *args, **kwargs) + + class TimeRangeField(RangeField): widget = DateRangeWidget diff --git a/django_filters/filters.py b/django_filters/filters.py index 4199b4d71..d3373d5c4 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -18,6 +18,7 @@ DateRangeField, DateTimeRangeField, IsoDateTimeField, + IsoDateTimeRangeField, LookupChoiceField, ModelChoiceField, ModelMultipleChoiceField, @@ -44,6 +45,7 @@ 'DurationFilter', 'Filter', 'IsoDateTimeFilter', + 'IsoDateTimeFromToRangeFilter', 'LookupChoiceFilter', 'ModelChoiceFilter', 'ModelMultipleChoiceFilter', @@ -271,7 +273,7 @@ class DateTimeFilter(Filter): class IsoDateTimeFilter(DateTimeFilter): """ - Uses IsoDateTimeField to support filtering on ISO 8601 formated datetimes. + Uses IsoDateTimeField to support filtering on ISO 8601 formatted datetimes. For context see: @@ -467,6 +469,10 @@ class DateTimeFromToRangeFilter(RangeFilter): field_class = DateTimeRangeField +class IsoDateTimeFromToRangeFilter(RangeFilter): + field_class = IsoDateTimeRangeField + + class TimeRangeFilter(RangeFilter): field_class = TimeRangeField diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 986bbeeaa..2d10c553d 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -565,6 +565,39 @@ Example:: f = F({'published_before': '2016-01-01 10:00'}) assert len(f.qs) == 2 +``IsoDateTimeFromToRangeFilter`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to a ``RangeFilter`` except it uses ISO 8601 formatted values instead of numerical values. It can be used with ``IsoDateTimeField``. + +Example:: + + class Article(models.Model): + published = dajngo_filters.IsoDateTimeField() + + class F(FilterSet): + published = IsoDateTimeFromToRangeFilter() + + class Meta: + model = Article + fields = ['published'] + + Article.objects.create(published='2016-01-01T8:00:00+01:00') + Article.objects.create(published='2016-01-01T9:30:00+01:00') + Article.objects.create(published='2016-01-02T8:00:00+01:00') + + # Range: Articles published 2016-01-01 between 8:00 and 10:00 + f = F({'published_after': '2016-01-01T8:00:00+01:00', 'published_before': '2016-01-01T10:00:00+01:00'}) + assert len(f.qs) == 2 + + # Min-Only: Articles published after 2016-01-01 8:00 + f = F({'published_after': '2016-01-01T8:00:00+01:00'}) + assert len(f.qs) == 3 + + # Max-Only: Articles published before 2016-01-01 10:00 + f = F({'published_before': '2016-01-01T10:00:00+0100'}) + assert len(f.qs) == 2 + ``TimeRangeFilter`` ~~~~~~~~~~~~~~~~~~~ diff --git a/tests/test_fields.py b/tests/test_fields.py index 6c0bf0417..9f6f8661c 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -12,6 +12,7 @@ DateRangeField, DateTimeRangeField, IsoDateTimeField, + IsoDateTimeRangeField, Lookup, LookupChoiceField, RangeField, @@ -90,6 +91,22 @@ def test_clean(self): datetime(2015, 1, 10, 8, 45, 0))) +class IsoDateTimeRangeFieldTests(TestCase): + + def test_field(self): + f = IsoDateTimeRangeField() + self.assertEqual(len(f.fields), 2) + + @override_settings(USE_TZ=False) + def test_clean(self): + w = RangeWidget() + f = IsoDateTimeRangeField(widget=w) + self.assertEqual( + f.clean(['2015-01-01T10:30:01.123000+01:00', '2015-01-10T08:45:02.345000+01:00']), + slice(datetime(2015, 1, 1, 9, 30, 1, 123000), + datetime(2015, 1, 10, 7, 45, 2, 345000))) + + class TimeRangeFieldTests(TestCase): def test_field(self): diff --git a/tests/test_filtering.py b/tests/test_filtering.py index 538a682ed..576f7cdff 100644 --- a/tests/test_filtering.py +++ b/tests/test_filtering.py @@ -19,6 +19,7 @@ DateRangeFilter, DateTimeFromToRangeFilter, DurationFilter, + IsoDateTimeFromToRangeFilter, LookupChoiceFilter, ModelChoiceFilter, ModelMultipleChoiceFilter, @@ -977,6 +978,33 @@ class Meta: self.assertEqual(len(results.qs), 2) +class IsoDateTimeFromToRangeFilterTests(TestCase): + + def test_filtering(self): + tz = timezone.get_current_timezone() + Article.objects.create( + published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz)) + Article.objects.create( + published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz)) + Article.objects.create( + published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz)) + Article.objects.create( + published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz)) + + class F(FilterSet): + published = IsoDateTimeFromToRangeFilter() + + class Meta: + model = Article + fields = ['published'] + + dt = (datetime.datetime.now(tz=tz)) + results = F(data={ + 'published_after': '2016-01-02T10:00:00.000000' + dt.strftime("%z"), + 'published_before': '2016-01-03T19:00:00.000000' + dt.strftime("%z")}) + self.assertEqual(len(results.qs), 2) + + class TimeRangeFilterTests(TestCase): def test_filtering(self): diff --git a/tests/test_filters.py b/tests/test_filters.py index 8c64c3730..8c96a5e05 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -14,6 +14,7 @@ BaseCSVField, DateRangeField, DateTimeRangeField, + IsoDateTimeRangeField, Lookup, RangeField, TimeRangeField @@ -33,6 +34,7 @@ DateTimeFromToRangeFilter, DurationFilter, Filter, + IsoDateTimeFromToRangeFilter, LookupChoiceFilter, ModelChoiceFilter, ModelMultipleChoiceFilter, @@ -1155,6 +1157,52 @@ def test_filtering_ignores_lookup_expr(self): None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45))) +class IsoDateTimeFromToRangeFilterTests(TestCase): + + def test_default_field(self): + f = IsoDateTimeFromToRangeFilter() + field = f.field + self.assertIsInstance(field, IsoDateTimeRangeField) + + def test_filtering_range(self): + qs = mock.Mock(spec=['filter']) + value = mock.Mock( + start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45)) + f = IsoDateTimeFromToRangeFilter() + f.filter(qs, value) + qs.filter.assert_called_once_with( + None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45))) + + def test_filtering_start(self): + qs = mock.Mock(spec=['filter']) + value = mock.Mock(start=datetime(2015, 4, 7, 8, 30), stop=None) + f = IsoDateTimeFromToRangeFilter() + f.filter(qs, value) + qs.filter.assert_called_once_with(None__gte=datetime(2015, 4, 7, 8, 30)) + + def test_filtering_stop(self): + qs = mock.Mock(spec=['filter']) + value = mock.Mock(start=None, stop=datetime(2015, 9, 6, 11, 45)) + f = IsoDateTimeFromToRangeFilter() + f.filter(qs, value) + qs.filter.assert_called_once_with(None__lte=datetime(2015, 9, 6, 11, 45)) + + def test_filtering_skipped_with_none_value(self): + qs = mock.Mock(spec=['filter']) + f = IsoDateTimeFromToRangeFilter() + result = f.filter(qs, None) + self.assertEqual(qs, result) + + def test_filtering_ignores_lookup_expr(self): + qs = mock.Mock() + value = mock.Mock( + start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45)) + f = IsoDateTimeFromToRangeFilter(lookup_expr='gte') + f.filter(qs, value) + qs.filter.assert_called_once_with( + None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45))) + + class TimeRangeFilterTests(TestCase): def test_default_field(self): From 99ced9ce401f8e08f4ee89a4e1246253640e5b2f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 17 Dec 2018 12:25:23 -0800 Subject: [PATCH 023/103] Use tox instead of detox (#996) Remove detox, since it's an unnecessary complicating factor. The majority of the build time is container creation in Travis. Adding tox run parallelization only adds nominal benefits. --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 61c2c8cf9..7b0dd85e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,11 @@ python: cache: pip install: - # latest detox and tox aren't compatible https://github.com/carltongibson/django-filter/issues/990 - - pip install coverage detox==0.13 tox~=3.2.1 tox-travis tox-venv + - pip install coverage tox tox-travis tox-venv script: - coverage erase - - detox + - tox matrix: fast_finish: true From 0cbb723479a99dbe8e48853e577b0feeff5c269d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 20 Jan 2019 13:11:43 +0100 Subject: [PATCH 024/103] Corrected FilterView behaviour with unbound FilterSet. (#1025) Closes #930, #987, #1007. --- django_filters/views.py | 2 +- tests/templates/tests/book_filter.html | 2 +- tests/test_views.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/django_filters/views.py b/django_filters/views.py index a4ba68a7e..81d77db1b 100644 --- a/django_filters/views.py +++ b/django_filters/views.py @@ -77,7 +77,7 @@ def get(self, request, *args, **kwargs): filterset_class = self.get_filterset_class() self.filterset = self.get_filterset(filterset_class) - if self.filterset.is_valid() or not self.get_strict(): + if not self.filterset.is_bound or self.filterset.is_valid() or not self.get_strict(): self.object_list = self.filterset.qs else: self.object_list = self.filterset.queryset.none() diff --git a/tests/templates/tests/book_filter.html b/tests/templates/tests/book_filter.html index 474ffebd8..b593ecd51 100644 --- a/tests/templates/tests/book_filter.html +++ b/tests/templates/tests/book_filter.html @@ -1,5 +1,5 @@ {{ filter.form }} -{% for obj in filter.qs %} +{% for obj in object_list %} {{ obj }} {% endfor %} diff --git a/tests/test_views.py b/tests/test_views.py index 1f9a56449..ed1009e6f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -123,6 +123,19 @@ class View(FilterView): self.assertEqual(message, expected) self.assertEqual(len(recorded), 0) + def test_view_with_unbound_filter_form_returns_initial_queryset(self): + factory = RequestFactory() + request = factory.get(self.base_url) + + queryset = Book.objects.filter(title='Snowcrash') + view = FilterView.as_view(model=Book, queryset=queryset) + + response = view(request) + titles = [o.title for o in response.context_data['object_list']] + + self.assertEqual(response.status_code, 200) + self.assertEqual(titles, ['Snowcrash']) + class GenericFunctionalViewTests(GenericViewTestCase): base_url = '/books-legacy/' From 6c3be993c401fbc6992a1cde02ee078ca9b576df Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 20 Jan 2019 16:03:16 +0100 Subject: [PATCH 025/103] Add testing against Django 2.2a1 Just py37 for now. docs 2.1 --- README.rst | 2 +- docs/guide/install.txt | 2 +- setup.py | 1 + tox.ini | 2 ++ 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 57597f043..5013cbd8e 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Requirements ------------ * **Python**: 3.4, 3.5, 3.6, 3.7 -* **Django**: 1.11, 2.0, 2.1 +* **Django**: 1.11, 2.0, 2.1, 2.2 * **DRF**: 3.8+ From Version 2.0 Django Filter is Python 3 only. diff --git a/docs/guide/install.txt b/docs/guide/install.txt index 503f830b6..0995b7a0b 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -30,5 +30,5 @@ __ http://www.django-rest-framework.org/ * **Python**: 3.4, 3.5, 3.6, 3.7 -* **Django**: 1.11, 2.0, 2.1 +* **Django**: 1.11, 2.0, 2.1, 2.2 * **DRF**: 3.8+ diff --git a/setup.py b/setup.py index 1c1e0f127..70fc5bf27 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', + 'Framework :: Django :: 2.2', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', diff --git a/tox.ini b/tox.ini index ffcbb0031..fb3a9f9b0 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = {py34,py35,py36}-django111, {py34,py35,py36}-django20, {py35,py36,py37}-django21, + {py37}-django22, {py35,py36,py37}-latest, isort,lint,docs,warnings, @@ -22,6 +23,7 @@ deps = django111: django>=1.11.0,<2.0 django20: django>=2.0,<2.1 django21: django>=2.1,<2.2 + django22: django==2.2a1 djangorestframework>=3.8,<3.9 latest: {[latest]deps} -rrequirements/test-ci.txt From c58979017ac261ec8e9cf203e571360751026d12 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 20 Jan 2019 20:34:00 +0100 Subject: [PATCH 026/103] Added change notes for v2.1. --- CHANGES.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f5d89a2a2..59d216a4a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,17 @@ +Version 2.1 (2019-1-20) +----------------------- + +* Fixed a regression in ``FilterView`` introduced in 2.0. An empty ``QuerySet`` was + incorrectly used whenever the FilterSet was unbound (i.e. when there were + no GET parameters). The correct, pre-2.0 behaviour is now restored. + + A workaround was to set ``strict=False`` on the ``FilterSet``. This is no + longer necessary, so you may restore `strict` behaviour as desired. + +* Added ``IsoDateTimeFromToRangeFilter``. Allows From-To filtering using + ISO-8601 formatted dates. + + Version 2.0 (2018-7-13) ----------------------- From 2a2da5e063b012a9fa2b22835e9353a021dc133f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 20 Jan 2019 20:39:03 +0100 Subject: [PATCH 027/103] Update version number for v2.1.0. --- .bumpversion.cfg | 2 +- django_filters/__init__.py | 2 +- docs/conf.py | 6 +++--- setup.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3437c70c7..e3db8f6a7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.0 +current_version = 2.1.0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? diff --git a/django_filters/__init__.py b/django_filters/__init__.py index 704596fc4..6ac799fd1 100644 --- a/django_filters/__init__.py +++ b/django_filters/__init__.py @@ -10,7 +10,7 @@ from . import rest_framework del pkgutil -__version__ = '2.0.0' +__version__ = '2.1.0' def parse_version(version): diff --git a/docs/conf.py b/docs/conf.py index b99765031..058bc9d53 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,16 +41,16 @@ # General information about the project. project = u'django-filter' -copyright = u'2013, Alex Gaynor and others.' +copyright = u'2019, Alex Gaynor, Carlton Gibson and others.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.0.0' +version = '2.1' # The full version, including alpha/beta/rc tags. -release = '2.0.0' +release = '2.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 70fc5bf27..a88c81b9a 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ readme = f.read() f.close() -version = '2.0.0' +version = '2.1.0' if sys.argv[-1] == 'publish': if os.system("pip freeze | grep wheel"): From 8e917de6c90d3d5512d71bae05dc463ecaebc88b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 20 Jan 2019 20:58:58 +0100 Subject: [PATCH 028/103] Comment Django 2.2 support in setup.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparently you can’t decalre that early… > Invalid value for classifiers. Error: 'Framework :: Django :: 2.2' is not a valid choice for this field. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a88c81b9a..46c8794fd 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', - 'Framework :: Django :: 2.2', +# 'Framework :: Django :: 2.2', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', From 9b6cfa7d6cf56db465f65115bb164aaf791f59e9 Mon Sep 17 00:00:00 2001 From: Jan Pieter Waagmeester Date: Mon, 21 Jan 2019 13:35:05 +0100 Subject: [PATCH 029/103] Fix indentation in docs/ref/filters.txt https://django-filter.readthedocs.io/en/master/ref/filters.html#datefromtorangefilter note/warning after code block was not properly indented --- docs/ref/filters.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 2d10c553d..d32893ad4 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -496,13 +496,13 @@ Example of using the ``DateField`` field:: # Max-Only: Comments added before 2016-02-01 f = F({'date_before': '2016-02-01'}) - .. note:: - When filtering ranges that occurs on DST transition dates ``DateFromToRangeFilter`` will use the first valid hour of the day for start datetime and the last valid hour of the day for end datetime. - This is OK for most applications, but if you want to customize this behavior you must extend ``DateFromToRangeFilter`` and make a custom field for it. +.. note:: + When filtering ranges that occurs on DST transition dates ``DateFromToRangeFilter`` will use the first valid hour of the day for start datetime and the last valid hour of the day for end datetime. + This is OK for most applications, but if you want to customize this behavior you must extend ``DateFromToRangeFilter`` and make a custom field for it. - .. warning:: - If you're using Django prior to 1.9 you may hit ``AmbiguousTimeError`` or ``NonExistentTimeError`` when start/end date matches DST start/end respectively. - This occurs because versions before 1.9 don't allow to change the DST behavior for making a datetime aware. +.. warning:: + If you're using Django prior to 1.9 you may hit ``AmbiguousTimeError`` or ``NonExistentTimeError`` when start/end date matches DST start/end respectively. + This occurs because versions before 1.9 don't allow to change the DST behavior for making a datetime aware. Example of using the ``DateTimeField`` field:: From 8351b85c8bd41348e19e8551a3348b7dc5598f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Mon, 21 Jan 2019 15:13:37 +0100 Subject: [PATCH 030/103] Revert "Comment Django 2.2 support in setup.py" The classifier is now available, see pypa/warehouse#5317 This reverts commit 8e917de6c90d3d5512d71bae05dc463ecaebc88b. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 46c8794fd..a88c81b9a 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', -# 'Framework :: Django :: 2.2', + 'Framework :: Django :: 2.2', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', From 37f56385112b035e67b5a553ead7ed3a20ba426a Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 4 Feb 2019 15:08:56 +0100 Subject: [PATCH 031/103] Drop testing Python 3.5 against Django master. (#1034) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fb3a9f9b0..ba31197e7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = {py34,py35,py36}-django20, {py35,py36,py37}-django21, {py37}-django22, - {py35,py36,py37}-latest, + {py36,py37}-latest, isort,lint,docs,warnings, From 7471b52a28740213f65f5374cf89f0c693f91c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ux=C3=ADo?= Date: Fri, 15 Feb 2019 12:13:07 +0100 Subject: [PATCH 032/103] Add range filters to Migration Guide (#1038) Closes #1037 --- docs/guide/migration.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/guide/migration.txt b/docs/guide/migration.txt index 3b75ea082..c09e6ac3f 100644 --- a/docs/guide/migration.txt +++ b/docs/guide/migration.txt @@ -117,6 +117,17 @@ example, ``RangeWidget`` now has ``_min`` and ``_max`` suffixes instead of ``_0`` and ``_1``. +Filters like ``RangeFilter, DateRangeFilter, DateTimeFromToRangeFilter...`` (`#770`__) +-------------------------------------------------------------------------------------- +__ https://github.com/carltongibson/django-filter/pull/770 + +As they depend on ``MultiWidget``, they need to be adjusted. In 1.0 release + parameters were provided using ``_0`` and ``_1`` as suffix``. For example, + a parameter ``creation_date`` using``DateRangeFilter`` will expect + ``creation_date_after`` and ``creation_date_before`` instead of + ``creation_date_0`` and ``creation_date_1``. + + ---------------- Migrating to 1.0 ---------------- From bd5da46a11160022de4659b3280315cdcb9e0114 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sat, 23 Feb 2019 14:56:21 +0100 Subject: [PATCH 033/103] Updated links to Django's stable docs. (#1043) --- django_filters/utils.py | 2 +- docs/guide/usage.txt | 4 ++-- docs/ref/fields.txt | 2 +- docs/ref/filters.txt | 6 +++--- docs/ref/filterset.txt | 4 ++-- tests/test_filterset.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/django_filters/utils.py b/django_filters/utils.py index dcd854ece..fbf58472b 100644 --- a/django_filters/utils.py +++ b/django_filters/utils.py @@ -184,7 +184,7 @@ def resolve_field(model_field, lookup_expr): This method is based on django.db.models.sql.query.Query.build_lookup For more info on the lookup API: - https://docs.djangoproject.com/en/1.9/ref/models/lookups/ + https://docs.djangoproject.com/en/stable/ref/models/lookups/ """ query = model_field.model._default_manager.all().query diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 7b9470b6f..30180a44e 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -75,14 +75,14 @@ There are two main arguments for filters: syntax can again be used in order to support lookup transforms. ex, ``year__gte``. -.. _`field lookup`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups +.. _`field lookup`: https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups Together, the field ``field_name`` and ``lookup_expr`` represent a complete Django lookup expression. A detailed explanation of lookup expressions is provided in Django's `lookup reference`_. django-filter supports expressions containing both transforms and a final lookup. -.. _`lookup reference`: https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups +.. _`lookup reference`: https://docs.djangoproject.com/en/stable/ref/models/lookups/#module-django.db.models.lookups Generating filters with Meta.fields diff --git a/docs/ref/fields.txt b/docs/ref/fields.txt index d73c4933f..236b492e0 100644 --- a/docs/ref/fields.txt +++ b/docs/ref/fields.txt @@ -19,5 +19,5 @@ You may set ``input_formats`` to your list of required formats as per the `DateT f.input_formats = [IsoDateTimeField.ISO_8601] + DateTimeField.input_formats -.. _`DateTimeField Docs`: https://docs.djangoproject.com/en/1.8/ref/forms/fields/#django.forms.DateTimeField.input_formats +.. _`DateTimeField Docs`: https://docs.djangoproject.com/en/stable/ref/forms/fields/#django.forms.DateTimeField.input_formats diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index d32893ad4..54d2ce8f0 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -13,7 +13,7 @@ The following are the core arguments that apply to all filters. Note that they are joined to construct the complete `lookup expression`_ that is the left hand side of the ORM ``.filter()`` call. -.. _`lookup expression`: https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups +.. _`lookup expression`: https://docs.djangoproject.com/en/stable/ref/models/lookups/#module-django.db.models.lookups ``field_name`` ~~~~~~~~~~~~~~ @@ -31,7 +31,7 @@ The `field lookup`_ that should be performed in the filter call. Defaults to are joined by the ORM lookup separator (``__``). e.g., filter a datetime by its year part ``year__gt``. -.. _`Field lookup`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups +.. _`Field lookup`: https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups .. _keyword-only-arguments: @@ -433,7 +433,7 @@ widget used is the ``RangeField``. Regular field lookups are available in addition to several containment lookups, including ``overlap``, ``contains``, and ``contained_by``. More details in the Django `docs`__. -__ https://docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields/#querying-range-fields +__ https://docs.djangoproject.com/en/stable/ref/contrib/postgres/fields/#querying-range-fields If the lower limit value is provided, the filter automatically defaults to ``startswith`` as the lookup and ``endswith`` if only the upper limit value is provided. diff --git a/docs/ref/filterset.txt b/docs/ref/filterset.txt index dcdf668ad..9100c0106 100644 --- a/docs/ref/filterset.txt +++ b/docs/ref/filterset.txt @@ -25,7 +25,7 @@ based on the underlying model field's type. This option must be combined with either the ``fields`` or ``exclude`` option, which is the same requirement for Django's ``ModelForm`` class, detailed `here`__. -__ https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#selecting-the-fields-to-use +__ https://docs.djangoproject.com/en/stable/topics/forms/modelforms/#selecting-the-fields-to-use .. code-block:: python @@ -69,7 +69,7 @@ in ``fields``. The dictionary syntax will create a filter for each lookup expression declared for its corresponding model field. These expressions may include both transforms and lookups, as detailed in the `lookup reference`__. -__ https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups +__ https://docs.djangoproject.com/en/stable/ref/models/lookups/#module-django.db.models.lookups .. _exclude: diff --git a/tests/test_filterset.py b/tests/test_filterset.py index 9fd8aa4cb..23460eca6 100644 --- a/tests/test_filterset.py +++ b/tests/test_filterset.py @@ -851,7 +851,7 @@ class MiscFilterSetTests(TestCase): def test_no__getitem__(self): # The DTL processes variable lookups by the following rules: - # https://docs.djangoproject.com/en/1.9/ref/templates/language/#variables + # https://docs.djangoproject.com/en/stable/ref/templates/language/#variables # A __getitem__ implementation precedes normal attribute access, and in # the case of #58, will force the queryset to evaluate when it should # not (eg, when rendering a blank form). From 0743ff57e60129982c62775238facb9140507c63 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sat, 23 Feb 2019 15:00:51 +0100 Subject: [PATCH 034/103] Azure: limit Python 3.5 builds to supported versions. --- .azure_pipelines/azure-pipelines.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.azure_pipelines/azure-pipelines.yml b/.azure_pipelines/azure-pipelines.yml index c58e9a58b..ca7126c40 100644 --- a/.azure_pipelines/azure-pipelines.yml +++ b/.azure_pipelines/azure-pipelines.yml @@ -9,6 +9,7 @@ strategy: # PYTHON_VERSION: '3.4' PY35: PYTHON_VERSION: '3.5' + TOXENV: django111, django20, django21 PY36: PYTHON_VERSION: '3.6' PY36_isort: @@ -38,7 +39,7 @@ steps: pip --version tox --skip-missing-interpreters true displayName: Run tox - + - script: | coverage combine coverage report -m From ab31272025a78c77330ffa105456bc0a522fd92d Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Wed, 6 Mar 2019 15:40:32 +0600 Subject: [PATCH 035/103] updates drf deps (#1051) --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index ba31197e7..46748b6f3 100644 --- a/tox.ini +++ b/tox.ini @@ -23,8 +23,8 @@ deps = django111: django>=1.11.0,<2.0 django20: django>=2.0,<2.1 django21: django>=2.1,<2.2 - django22: django==2.2a1 - djangorestframework>=3.8,<3.9 + django22: django==2.2b1 + djangorestframework>=3.9.2,<3.10 latest: {[latest]deps} -rrequirements/test-ci.txt From 3027c9ea3ed4371d209ad022544988c008c45904 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 8 Mar 2019 16:27:14 +0100 Subject: [PATCH 036/103] Fix for isort>4.3.10. (#1055) --- django_filters/__init__.py | 2 +- django_filters/rest_framework/__init__.py | 2 +- django_filters/rest_framework/backends.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/django_filters/__init__.py b/django_filters/__init__.py index 6ac799fd1..399b6d238 100644 --- a/django_filters/__init__.py +++ b/django_filters/__init__.py @@ -1,8 +1,8 @@ # flake8: noqa import pkgutil -from .filterset import FilterSet from .filters import * +from .filterset import FilterSet # We make the `rest_framework` module available without an additional import. # If DRF is not installed, no-op. diff --git a/django_filters/rest_framework/__init__.py b/django_filters/rest_framework/__init__.py index 55025e705..4ffc4084e 100644 --- a/django_filters/rest_framework/__init__.py +++ b/django_filters/rest_framework/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa from .backends import DjangoFilterBackend -from .filterset import FilterSet from .filters import * +from .filterset import FilterSet diff --git a/django_filters/rest_framework/backends.py b/django_filters/rest_framework/backends.py index d401290d7..f13035adb 100644 --- a/django_filters/rest_framework/backends.py +++ b/django_filters/rest_framework/backends.py @@ -3,8 +3,8 @@ from django.template import loader from django.utils.deprecation import RenameMethodsBase -from . import filters, filterset from .. import compat, utils +from . import filters, filterset # TODO: remove metaclass in 2.1 From 9d921c726895d111b99604c914e50c36fe14091e Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 8 Mar 2019 22:39:03 +0100 Subject: [PATCH 037/103] Add lookup_expr to MultipleChoiceFilter (#1054) This was requested in #49 (back when lookup_expr was called lookup_type). This is a minor change, with the added advantage of making ``MultipleChoiceFilter`` perform more like other Filter classes, down to the explicit ``__exact`` filter. As the documentation only mentions ``lookup_expr`` to be available on Filter classes in general, and doesn't mention ``MultipleChoiceFilter`` to be different, no documentation change is necessary. --- django_filters/filters.py | 7 +++++-- tests/test_filters.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/django_filters/filters.py b/django_filters/filters.py index d3373d5c4..36ed3df08 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -253,10 +253,13 @@ def filter(self, qs, value): return qs.distinct() if self.distinct else qs def get_filter_predicate(self, v): + name = self.field_name + if name and self.lookup_expr != 'exact': + name = LOOKUP_SEP.join([name, self.lookup_expr]) try: - return {self.field_name: getattr(v, self.field.to_field_name)} + return {name: getattr(v, self.field.to_field_name)} except (AttributeError, TypeError): - return {self.field_name: v} + return {name: v} class TypedMultipleChoiceFilter(MultipleChoiceFilter): diff --git a/tests/test_filters.py b/tests/test_filters.py index 8c96a5e05..4214fadda 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -419,6 +419,21 @@ def test_filtering_exclude(self): qs.exclude.assert_called_once_with(mockQ1.__ior__.return_value) qs.exclude.return_value.distinct.assert_called_once_with() + def test_filtering_with_lookup_expr(self): + qs = mock.Mock(spec=['filter']) + f = MultipleChoiceFilter(field_name='somefield', lookup_expr='icontains') + with mock.patch('django_filters.filters.Q') as mockQclass: + mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() + mockQclass.side_effect = [mockQ1, mockQ2] + + f.filter(qs, ['value']) + + self.assertEqual(mockQclass.call_args_list, + [mock.call(), mock.call(somefield__icontains='value')]) + mockQ1.__ior__.assert_called_once_with(mockQ2) + qs.filter.assert_called_once_with(mockQ1.__ior__.return_value) + qs.filter.return_value.distinct.assert_called_once_with() + def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices(self): qs = mock.Mock(spec=[]) f = MultipleChoiceFilter(field_name='somefield', required=True) From 81ad70d6dd6f0e427ffc0a99633ef9024d04e277 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Mon, 1 Apr 2019 23:18:23 +0600 Subject: [PATCH 038/103] Updated tox to Django 2.2. (#1063) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 46748b6f3..5aef7510a 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ deps = django111: django>=1.11.0,<2.0 django20: django>=2.0,<2.1 django21: django>=2.1,<2.2 - django22: django==2.2b1 + django22: django==2.2,<3.0 djangorestframework>=3.9.2,<3.10 latest: {[latest]deps} -rrequirements/test-ci.txt From 9f86ae2e95c93153d5bd52f4b2ada83f766a5cf3 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 4 Apr 2019 09:37:50 -0700 Subject: [PATCH 039/103] Cleanup XML test runner output (#1060) * Put xml coverage files into subdirectory * Expect IsoDateTimeFromToRangeFilterTests to fail --- .gitignore | 1 + tests/settings.py | 4 ++++ tests/test_filtering.py | 1 + 3 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index c0fafb1bc..0967c42d4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ docs/_build .tox .coverage .coverage.* +.xmlcoverage/ diff --git a/tests/settings.py b/tests/settings.py index 3892cbf8c..c1ec0815b 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -36,6 +36,10 @@ STATIC_URL = '/static/' +# XMLTestRunner output +TEST_OUTPUT_DIR = '.xmlcoverage' + + # help verify that DEFAULTS is importable from conf. def FILTERS_VERBOSE_LOOKUPS(): return DEFAULTS['VERBOSE_LOOKUPS'] diff --git a/tests/test_filtering.py b/tests/test_filtering.py index 576f7cdff..18f2b4821 100644 --- a/tests/test_filtering.py +++ b/tests/test_filtering.py @@ -978,6 +978,7 @@ class Meta: self.assertEqual(len(results.qs), 2) +@unittest.expectedFailure class IsoDateTimeFromToRangeFilterTests(TestCase): def test_filtering(self): From 24adad8c48bc9e7c7539b6510ffde4ce4effdc29 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 5 Apr 2019 06:54:39 -0700 Subject: [PATCH 040/103] Pin timezone to UTC (#1058) --- tests/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/settings.py b/tests/settings.py index c1ec0815b..f0af9c768 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -25,6 +25,8 @@ USE_TZ = True +TIME_ZONE = 'UTC' + SECRET_KEY = 'foobar' TEMPLATES = [{ From 7c08deea132686f456df8a646a9c65654cee2712 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 24 May 2019 17:12:24 +0200 Subject: [PATCH 041/103] Bump pyyaml from 3.11 to 5.1 in /requirements (#1079) Bumps [pyyaml](https://github.com/yaml/pyyaml) from 3.11 to 5.1. - [Release notes](https://github.com/yaml/pyyaml/releases) - [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES) - [Commits](https://github.com/yaml/pyyaml/compare/3.11...5.1) --- requirements/maintainer.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/maintainer.txt b/requirements/maintainer.txt index e2b43a9d8..539fded65 100644 --- a/requirements/maintainer.txt +++ b/requirements/maintainer.txt @@ -14,7 +14,7 @@ pbr==1.7.0 pkginfo==1.2.1 Pygments==2.1.3 pytz==2016.6.1 -PyYAML==3.11 +PyYAML==5.1 requests==2.20.0 six==1.9.0 snowballstemmer==1.2.1 From 5f00165c7e6fbc84a70fdd21824dd6466281a616 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 15 Jul 2019 21:14:11 +0200 Subject: [PATCH 042/103] Fixed test_views failures with Django 3.0+. (#1096) django.utils.html.escape() converts ' to its decimal code "'" instead of the equivalent hex code "'". Behavior has changed in https://github.com/django/django/commit/8d76443aba863b75ad3b1392ca7e1d59bad84dc4 --- tests/test_views.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index ed1009e6f..c6bd776e3 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -3,6 +3,7 @@ from django.core.exceptions import ImproperlyConfigured from django.test import TestCase, override_settings from django.test.client import RequestFactory +from django.utils import html from django_filters.filterset import FilterSet, filterset_factory from django_filters.views import FilterView @@ -27,13 +28,13 @@ class GenericClassBasedViewTests(GenericViewTestCase): def test_view(self): response = self.client.get(self.base_url) - for b in ['Ender's Game', 'Rainbow Six', 'Snowcrash']: - self.assertContains(response, b) + for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + self.assertContains(response, html.escape(b)) def test_view_filtering_on_title(self): response = self.client.get(self.base_url + '?title=Snowcrash') - for b in ['Ender's Game', 'Rainbow Six']: - self.assertNotContains(response, b) + for b in ["Ender's Game", 'Rainbow Six']: + self.assertNotContains(response, html.escape(b)) self.assertContains(response, 'Snowcrash') def test_view_with_filterset_not_model(self): @@ -43,8 +44,8 @@ def test_view_with_filterset_not_model(self): view = FilterView.as_view(filterset_class=filterset) response = view(request) self.assertEqual(response.status_code, 200) - for b in ['Ender's Game', 'Rainbow Six', 'Snowcrash']: - self.assertContains(response, b) + for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + self.assertContains(response, html.escape(b)) def test_view_with_model_no_filterset(self): factory = RequestFactory() @@ -52,8 +53,8 @@ def test_view_with_model_no_filterset(self): view = FilterView.as_view(model=Book) response = view(request) self.assertEqual(response.status_code, 200) - for b in ['Ender's Game', 'Rainbow Six', 'Snowcrash']: - self.assertContains(response, b) + for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + self.assertContains(response, html.escape(b)) def test_view_with_model_and_fields_no_filterset(self): factory = RequestFactory() @@ -63,15 +64,15 @@ def test_view_with_model_and_fields_no_filterset(self): # filtering only by price response = view(request) self.assertEqual(response.status_code, 200) - for b in ['Ender's Game', 'Rainbow Six', 'Snowcrash']: - self.assertContains(response, b) + for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + self.assertContains(response, html.escape(b)) # not filtering by title request = factory.get(self.base_url + '?title=Snowcrash') response = view(request) self.assertEqual(response.status_code, 200) - for b in ['Ender's Game', 'Rainbow Six', 'Snowcrash']: - self.assertContains(response, b) + for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + self.assertContains(response, html.escape(b)) def test_view_with_strict_errors(self): factory = RequestFactory() @@ -142,14 +143,14 @@ class GenericFunctionalViewTests(GenericViewTestCase): def test_view(self): response = self.client.get(self.base_url) - for b in ['Ender's Game', 'Rainbow Six', 'Snowcrash']: - self.assertContains(response, b) + for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + self.assertContains(response, html.escape(b)) # extra context self.assertEqual(response.context_data['foo'], 'bar') self.assertEqual(response.context_data['bar'], 'foo') def test_view_filtering_on_price(self): response = self.client.get(self.base_url + '?title=Snowcrash') - for b in ['Ender's Game', 'Rainbow Six']: - self.assertNotContains(response, b) + for b in ["Ender's Game", 'Rainbow Six']: + self.assertNotContains(response, html.escape(b)) self.assertContains(response, 'Snowcrash') From fc94eb8ed24599afd796bd0024674fbc7b99aaf7 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 15 Jul 2019 21:16:53 +0200 Subject: [PATCH 043/103] Updated links to DRF and django-filter GitHub pages. (#1095) --- django_filters/filters.py | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/django_filters/filters.py b/django_filters/filters.py index 36ed3df08..47159ad5d 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -281,8 +281,8 @@ class IsoDateTimeFilter(DateTimeFilter): For context see: * https://code.djangoproject.com/ticket/23448 - * https://github.com/tomchristie/django-rest-framework/issues/1338 - * https://github.com/alex/django-filter/pull/264 + * https://github.com/encode/django-rest-framework/issues/1338 + * https://github.com/carltongibson/django-filter/pull/264 """ field_class = IsoDateTimeField diff --git a/tox.ini b/tox.ini index 5aef7510a..07b9fb424 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ envlist = [latest] deps = https://github.com/django/django/archive/master.tar.gz - https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz + https://github.com/encode/django-rest-framework/archive/master.tar.gz [testenv] commands = coverage run --parallel-mode --source django_filters ./runtests.py --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner {posargs} From c98e52d80d8deb626fe5f4826cf2872e79a86459 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Mon, 15 Jul 2019 15:18:59 -0400 Subject: [PATCH 044/103] Added get_schema_operation_parameters() for DRF OpenAPI schema generation. (#1086) * dummy get_schema_operation_parameters() so DRF generateschema won't throw an exception. * implement get_schema_operation_parameters() * Updated release note text. --- CHANGES.rst | 5 +++++ django_filters/rest_framework/backends.py | 22 ++++++++++++++++++++++ docs/guide/rest_framework.txt | 6 +++--- tests/rest_framework/test_backends.py | 9 +++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 59d216a4a..45cf24974 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +Version 2.x (unreleased) +------------------------ + +* Added ``DjangoFilterBackend.get_schema_operation_parameters()`` for DRF 3.10+ OpenAPI schema generation. + Version 2.1 (2019-1-20) ----------------------- diff --git a/django_filters/rest_framework/backends.py b/django_filters/rest_framework/backends.py index f13035adb..eed802a41 100644 --- a/django_filters/rest_framework/backends.py +++ b/django_filters/rest_framework/backends.py @@ -138,3 +138,25 @@ def get_schema_fields(self, view): schema=self.get_coreschema_field(field) ) for field_name, field in filterset_class.base_filters.items() ] + + def get_schema_operation_parameters(self, view): + try: + queryset = view.get_queryset() + except Exception: + queryset = None + warnings.warn( + "{} is not compatible with schema generation".format(view.__class__) + ) + + filterset_class = self.get_filterset_class(view, queryset) + return [] if not filterset_class else [ + ({ + 'name': field_name, + 'required': field.extra['required'], + 'in': 'query', + 'description': field.label if field.label is not None else field_name, + 'schema': { + 'type': 'string', + }, + }) for field_name, field in filterset_class.base_filters.items() + ] diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index 69e336edd..2384049c1 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -148,10 +148,10 @@ You can override these methods on a case-by-case basis for each view, creating u 'author': self.get_author(), } -Schema Generation with Core API -------------------------------- +Schema Generation with Core API and Open API +-------------------------------------------- -The backend class integrates with DRF's schema generation by implementing ``get_schema_fields()``. This is automatically enabled when Core API is installed. Schema generation usually functions seamlessly, however the implementation does expect to invoke the view's ``get_queryset()`` method. There is a caveat in that views are artificially constructed during schema generation, so the ``args`` and ``kwargs`` attributes will be empty. If you depend on arguments parsed from the URL, you will need to handle their absence in ``get_queryset()``. +The backend class integrates with DRF's schema generation by implementing ``get_schema_fields()`` and ``get_schema_operation_parameters()``. ``get_schema_fields()`` is automatically enabled when Core API is installed. ``get_schema_operation_parameters()`` is always enabled for Open API (new since DRF 3.9). Schema generation usually functions seamlessly, however the implementation does expect to invoke the view's ``get_queryset()`` method. There is a caveat in that views are artificially constructed during schema generation, so the ``args`` and ``kwargs`` attributes will be empty. If you depend on arguments parsed from the URL, you will need to handle their absence in ``get_queryset()``. For example, your get queryset method may look like this: diff --git a/tests/rest_framework/test_backends.py b/tests/rest_framework/test_backends.py index cf5abe841..429ccfc3a 100644 --- a/tests/rest_framework/test_backends.py +++ b/tests/rest_framework/test_backends.py @@ -229,6 +229,15 @@ class View(FilterClassRootView): self.assertEqual(fields, ['text', 'decimal', 'date', 'f']) +class GetSchemaOperationParametersTests(TestCase): + def test_get_operation_parameters_with_filterset_fields_list(self): + backend = DjangoFilterBackend() + fields = backend.get_schema_operation_parameters(FilterFieldsRootView()) + fields = [f['name'] for f in fields] + + self.assertEqual(fields, ['decimal', 'date']) + + class TemplateTests(TestCase): def test_backend_output(self): """ From 382f6a4114f70c602c4cd2b6dc77156afeea79f0 Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Mon, 8 Jul 2019 02:39:23 +0200 Subject: [PATCH 045/103] Simply versions in Tox --- tox.ini | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 07b9fb424..3c5a2eac8 100644 --- a/tox.ini +++ b/tox.ini @@ -20,11 +20,11 @@ ignore_outcome = setenv = PYTHONDONTWRITEBYTECODE=1 deps = - django111: django>=1.11.0,<2.0 - django20: django>=2.0,<2.1 - django21: django>=2.1,<2.2 - django22: django==2.2,<3.0 - djangorestframework>=3.9.2,<3.10 + django111: django==1.11.* + django20: django==2.0.* + django21: django==2.1.* + django22: django==2.2.* + djangorestframework==3.9.* latest: {[latest]deps} -rrequirements/test-ci.txt From fde4de156c2c3ce02173cbd6015a9cf4d7ad663b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 15 Jul 2019 21:15:40 +0200 Subject: [PATCH 046/103] Update DRF requirement for 3.10 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3c5a2eac8..a44e6a01e 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ deps = django20: django==2.0.* django21: django==2.1.* django22: django==2.2.* - djangorestframework==3.9.* + djangorestframework==3.10.* latest: {[latest]deps} -rrequirements/test-ci.txt From 524570cc6e926359659bb42f4ae7e1a58544a94d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 15 Jul 2019 21:22:51 +0200 Subject: [PATCH 047/103] Drop testing against EOL Python 3.4 --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index a44e6a01e..6e2d6c700 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - {py34,py35,py36}-django111, - {py34,py35,py36}-django20, + {py35,py36}-django111, + {py35,py36}-django20, {py35,py36,py37}-django21, {py37}-django22, {py36,py37}-latest, From e5d95b2e83cc3b9f9d53d800f8f3365d8c1d74ad Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Mon, 15 Jul 2019 21:37:51 +0200 Subject: [PATCH 048/103] Drop py34 from travis config Co-author: @rpkilby --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7b0dd85e3..8adaaa3b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ sudo: false dist: xenial language: python python: - - "3.4" - "3.5" - "3.6" - "3.7" From d41b70521982e321d5170f028c93f7ac7b588eab Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Mon, 15 Jul 2019 21:39:02 +0200 Subject: [PATCH 049/103] Add py35,py36 to the django22 tox matrix Co-Author: @rpkilby --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6e2d6c700..45189dc02 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = {py35,py36}-django111, {py35,py36}-django20, {py35,py36,py37}-django21, - {py37}-django22, + {py35,py36,py37}-django22, {py36,py37}-latest, isort,lint,docs,warnings, From 5d1f6a512c0c8f092897bf805a3b3990451202e8 Mon Sep 17 00:00:00 2001 From: Jan Pieter Waagmeester Date: Tue, 16 Jul 2019 11:34:58 +0200 Subject: [PATCH 050/103] Replaced ugettext_lazy with gettext_lazy, force_text with force_str. (#1075) Fixes - `RemovedInDjango40Warning: django.utils.translation.ugettext_lazy() is deprecated in favor of django.utils.translation.gettext_lazy().` - `RemovedInDjango40Warning: force_text() is deprecated in favor of force_str().` --- django_filters/conf.py | 2 +- django_filters/fields.py | 2 +- django_filters/filters.py | 2 +- django_filters/rest_framework/filterset.py | 2 +- django_filters/utils.py | 10 +++++----- django_filters/widgets.py | 12 ++++++------ tests/models.py | 2 +- tests/rest_framework/models.py | 2 +- tests/test_filters.py | 3 +-- 9 files changed, 18 insertions(+), 19 deletions(-) diff --git a/django_filters/conf.py b/django_filters/conf.py index 81d710785..12bd4b229 100644 --- a/django_filters/conf.py +++ b/django_filters/conf.py @@ -1,6 +1,6 @@ from django.conf import settings as dj_settings from django.core.signals import setting_changed -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .utils import deprecate diff --git a/django_filters/fields.py b/django_filters/fields.py index 617055e4d..930bba473 100644 --- a/django_filters/fields.py +++ b/django_filters/fields.py @@ -4,7 +4,7 @@ from django import forms from django.utils.dateparse import parse_datetime from django.utils.encoding import force_str -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .conf import settings from .constants import EMPTY_VALUES diff --git a/django_filters/filters.py b/django_filters/filters.py index 47159ad5d..7331bfd1d 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -7,7 +7,7 @@ from django.forms.utils import pretty_name from django.utils.itercompat import is_iterable from django.utils.timezone import now -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .conf import settings from .constants import EMPTY_VALUES diff --git a/django_filters/rest_framework/filterset.py b/django_filters/rest_framework/filterset.py index 376a50920..8c4230422 100644 --- a/django_filters/rest_framework/filterset.py +++ b/django_filters/rest_framework/filterset.py @@ -1,7 +1,7 @@ from copy import deepcopy from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django_filters import filterset diff --git a/django_filters/utils.py b/django_filters/utils.py index fbf58472b..19da34a74 100644 --- a/django_filters/utils.py +++ b/django_filters/utils.py @@ -10,9 +10,9 @@ from django.db.models.fields import FieldDoesNotExist from django.db.models.fields.related import ForeignObjectRel, RelatedField from django.utils import timezone -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.text import capfirst -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from .exceptions import FieldLookupError @@ -253,7 +253,7 @@ def verbose_field_name(model, field_name): else: return '[invalid name]' else: - names.append(force_text(part.verbose_name)) + names.append(force_str(part.verbose_name)) return ' '.join(names) @@ -278,7 +278,7 @@ def verbose_lookup_expr(lookup_expr): VERBOSE_LOOKUPS = app_settings.VERBOSE_LOOKUPS or {} lookups = [ - force_text(VERBOSE_LOOKUPS.get(lookup, _(lookup))) + force_str(VERBOSE_LOOKUPS.get(lookup, _(lookup))) for lookup in lookup_expr.split(LOOKUP_SEP) ] @@ -302,7 +302,7 @@ def label_for_filter(model, field_name, lookup_expr, exclude=False): if isinstance(lookup_expr, str): verbose_expression += [verbose_lookup_expr(lookup_expr)] - verbose_expression = [force_text(part) for part in verbose_expression if part] + verbose_expression = [force_str(part) for part in verbose_expression if part] verbose_expression = capfirst(' '.join(verbose_expression)) return verbose_expression diff --git a/django_filters/widgets.py b/django_filters/widgets.py index 756f4f776..b22ceaeab 100644 --- a/django_filters/widgets.py +++ b/django_filters/widgets.py @@ -6,10 +6,10 @@ from django.db.models.fields import BLANK_CHOICE_DASH from django.forms.utils import flatatt from django.utils.datastructures import MultiValueDict -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.http import urlencode from django.utils.safestring import mark_safe -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ class LinkWidget(forms.Widget): @@ -37,7 +37,7 @@ def render(self, name, value, attrs=None, choices=(), renderer=None): return mark_safe('\n'.join(output)) def render_options(self, choices, selected_choices, name): - selected_choices = set(force_text(v) for v in selected_choices) + selected_choices = set(force_str(v) for v in selected_choices) output = [] for option_value, option_label in chain(self.choices, choices): if isinstance(option_label, (list, tuple)): @@ -52,7 +52,7 @@ def render_options(self, choices, selected_choices, name): def render_option(self, name, selected_choices, option_value, option_label): - option_value = force_text(option_value) + option_value = force_str(option_value) if option_label == BLANK_CHOICE_DASH[0][1]: option_label = _("All") data = self.data.copy() @@ -65,7 +65,7 @@ def render_option(self, name, selected_choices, return self.option_string() % { 'attrs': selected and ' class="selected"' or '', 'query_string': url, - 'label': force_text(option_label) + 'label': force_str(option_label) } def option_string(self): @@ -213,7 +213,7 @@ def render(self, name, value, attrs=None, renderer=None): # if we have multiple values, we need to force render as a text input # (otherwise, the additional values are lost) surrogate = forms.TextInput() - value = [force_text(surrogate.format_value(v)) for v in value] + value = [force_str(surrogate.format_value(v)) for v in value] value = ','.join(list(value)) return surrogate.render(name, value, attrs, renderer=renderer) diff --git a/tests/models.py b/tests/models.py index 1dc937a81..9a6f1040a 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,6 +1,6 @@ from django import forms from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ REGULAR = 0 MANAGER = 1 diff --git a/tests/rest_framework/models.py b/tests/rest_framework/models.py index 4be18b11f..d3a0446f9 100644 --- a/tests/rest_framework/models.py +++ b/tests/rest_framework/models.py @@ -1,6 +1,6 @@ from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class BasicModel(models.Model): diff --git a/tests/test_filters.py b/tests/test_filters.py index 4214fadda..396f47274 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import inspect import mock from collections import OrderedDict @@ -7,7 +6,7 @@ from django import forms from django.test import TestCase, override_settings from django.utils import translation -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django_filters import filters, widgets from django_filters.fields import ( From da4b64ea8dde19304b552592a32b343e9aefe9da Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 16 Jul 2019 20:22:40 +0200 Subject: [PATCH 051/103] Version 2.2. --- .bumpversion.cfg | 2 +- CHANGES.rst | 10 +++++++--- django_filters/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 3 +-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e3db8f6a7..2450fcd0d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.1.0 +current_version = 2.2.0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? diff --git a/CHANGES.rst b/CHANGES.rst index 45cf24974..7a2a41c76 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,11 @@ -Version 2.x (unreleased) ------------------------- +Version 2.2 (2019-7-16) +----------------------- + +* Added ``DjangoFilterBackend.get_schema_operation_parameters()`` for DRF 3.10+ + OpenAPI schema generation. (#1086) +* Added ``lookup_expr`` to ``MultipleChoiceFilter`` (#1054) +* Dropped support for EOL Python 3.4 -* Added ``DjangoFilterBackend.get_schema_operation_parameters()`` for DRF 3.10+ OpenAPI schema generation. Version 2.1 (2019-1-20) ----------------------- diff --git a/django_filters/__init__.py b/django_filters/__init__.py index 399b6d238..a1eb6ffda 100644 --- a/django_filters/__init__.py +++ b/django_filters/__init__.py @@ -10,7 +10,7 @@ from . import rest_framework del pkgutil -__version__ = '2.1.0' +__version__ = '2.2.0' def parse_version(version): diff --git a/docs/conf.py b/docs/conf.py index 058bc9d53..334e4c45c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '2.1' +version = '2.2' # The full version, including alpha/beta/rc tags. -release = '2.1.0' +release = '2.2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index a88c81b9a..af026e02d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ readme = f.read() f.close() -version = '2.1.0' +version = '2.2.0' if sys.argv[-1] == 'publish': if os.system("pip freeze | grep wheel"): @@ -49,7 +49,6 @@ 'Framework :: Django :: 2.2', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', From a5b4d0299ec04dcb103dc37291001fb268107c66 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 30 Sep 2019 12:22:51 +0200 Subject: [PATCH 052/103] Fixed import of FieldDoesNotExist. (#1127) --- django_filters/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/django_filters/utils.py b/django_filters/utils.py index 19da34a74..e0f7bab4b 100644 --- a/django_filters/utils.py +++ b/django_filters/utils.py @@ -3,11 +3,10 @@ import django from django.conf import settings -from django.core.exceptions import FieldError +from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import Expression -from django.db.models.fields import FieldDoesNotExist from django.db.models.fields.related import ForeignObjectRel, RelatedField from django.utils import timezone from django.utils.encoding import force_str From c07f297babfcb6e7bf5ffa95fb2f1bc8412387eb Mon Sep 17 00:00:00 2001 From: Baishakhi Dasgupta Date: Tue, 29 Oct 2019 21:09:33 +0530 Subject: [PATCH 053/103] Added testing against Django 3.0. (#1125) --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 45189dc02..9b9229918 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = {py35,py36}-django20, {py35,py36,py37}-django21, {py35,py36,py37}-django22, + {py36,py37}-django30, {py36,py37}-latest, isort,lint,docs,warnings, @@ -24,6 +25,7 @@ deps = django20: django==2.0.* django21: django==2.1.* django22: django==2.2.* + django30: django>=3.0a1,<3.1 djangorestframework==3.10.* latest: {[latest]deps} -rrequirements/test-ci.txt From aa7f7ff508d07d6d6ebf28c5b3eaec00e0c09f3e Mon Sep 17 00:00:00 2001 From: Bastien Vallet Date: Tue, 29 Oct 2019 16:49:51 +0100 Subject: [PATCH 054/103] Declared support for, and added testing against, Python 3.8. (#1138) --- .travis.yml | 5 +++-- setup.py | 3 ++- tox.ini | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8adaaa3b6..7dac3eba1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - "3.5" - "3.6" - "3.7" + - "3.8" cache: pip @@ -19,9 +20,9 @@ script: matrix: fast_finish: true include: - - python: "3.7" + - python: "3.8" env: TOXENV=isort,lint,docs - - python: "3.7" + - python: "3.8" env: TOXENV=warnings allow_failures: - env: TOXENV=warnings diff --git a/setup.py b/setup.py index af026e02d..412c99a95 100644 --- a/setup.py +++ b/setup.py @@ -52,10 +52,11 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Framework :: Django', ], zip_safe=False, - python_requires='>=3.4', + python_requires='>=3.5', install_requires=[ 'Django>=1.11', ], diff --git a/tox.ini b/tox.ini index 9b9229918..d27f75615 100644 --- a/tox.ini +++ b/tox.ini @@ -4,8 +4,8 @@ envlist = {py35,py36}-django20, {py35,py36,py37}-django21, {py35,py36,py37}-django22, - {py36,py37}-django30, - {py36,py37}-latest, + {py36,py37,38}-django30, + {py36,py37,38}-latest, isort,lint,docs,warnings, From 805bd07400ff8f9eb72ef878a8880e63135a71d7 Mon Sep 17 00:00:00 2001 From: andreage Date: Thu, 14 Nov 2019 18:15:34 +0100 Subject: [PATCH 055/103] Updated `super()` to new usage in the docs (#1147) --- docs/guide/tips.txt | 4 ++-- docs/guide/usage.txt | 2 +- docs/ref/filters.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index 76563028e..eccdc4d30 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -173,7 +173,7 @@ for magic values. This is similar to the ``ChoiceFilter``'s null value handling. def filter(self, qs, value): if value != self.empty_value: - return super(MyCharFilter, self).filter(qs, value) + return super().filter(qs, value) qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): ""}) return qs.distinct() if self.distinct else qs @@ -243,4 +243,4 @@ If defaults are necessary though, the following should mimic the pre-1.0 behavio if not data.get(name) and initial: data[name] = initial - super(BaseFilterSet, self).__init__(data, *args, **kwargs) + super().__init__(data, *args, **kwargs) diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 30180a44e..68fb21a96 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -197,7 +197,7 @@ those that are published and those that are owned by the logged-in user @property def qs(self): - parent = super(ArticleFilter, self).qs + parent = super().qs author = getattr(self.request, 'user', None) return parent.filter(is_published=True) \ diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 54d2ce8f0..e534a896f 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -814,7 +814,7 @@ If you wish to sort by non-model fields, you'll need to add custom handling to a class CustomOrderingFilter(django_filters.OrderingFilter): def __init__(self, *args, **kwargs): - super(CustomOrderingFilter, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.extra['choices'] += [ ('relevance', 'Relevance'), ('-relevance', 'Relevance (descending)'), @@ -827,4 +827,4 @@ If you wish to sort by non-model fields, you'll need to add custom handling to a # sort queryset by relevance return ... - return super(CustomOrderingFilter, self).filter(qs, value) + return super().filter(qs, value) From 33201e31cc3f83588c696664cc1e67a379013a0c Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 14 Nov 2019 17:16:39 +0000 Subject: [PATCH 056/103] Update list of supported versions (#1148) --- README.rst | 6 +++--- docs/guide/install.txt | 6 +++--- setup.py | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 5013cbd8e..685fbad18 100644 --- a/README.rst +++ b/README.rst @@ -21,9 +21,9 @@ Full documentation on `read the docs`_. Requirements ------------ -* **Python**: 3.4, 3.5, 3.6, 3.7 -* **Django**: 1.11, 2.0, 2.1, 2.2 -* **DRF**: 3.8+ +* **Python**: 3.5, 3.6, 3.7, 3.8 +* **Django**: 1.11, 2.0, 2.1, 2.2, 3.0 +* **DRF**: 3.10+ From Version 2.0 Django Filter is Python 3 only. If you need to support Python 2.7 use the version 1.1 release. diff --git a/docs/guide/install.txt b/docs/guide/install.txt index 0995b7a0b..adadc2655 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -29,6 +29,6 @@ __ http://www.django-rest-framework.org/ -* **Python**: 3.4, 3.5, 3.6, 3.7 -* **Django**: 1.11, 2.0, 2.1, 2.2 -* **DRF**: 3.8+ +* **Python**: 3.5, 3.6, 3.7, 3.8 +* **Django**: 1.11, 2.0, 2.1, 2.2, 3.0 +* **DRF**: 3.10+ diff --git a/setup.py b/setup.py index 412c99a95..4476d6169 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', + 'Framework :: Django :: 3.0', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', From 2abaf99882e3718643a8eb772582a51d52a19722 Mon Sep 17 00:00:00 2001 From: Jan <1952703+najitlaw@users.noreply.github.com> Date: Thu, 14 Nov 2019 18:18:20 +0100 Subject: [PATCH 057/103] Docs: Updated field_name example in tips.txt. (#1141) There is no field "name" on the Filter class, should be "field_name". Exception: Exception Value: 'MyCharFilter' object has no attribute 'name' --- docs/guide/tips.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index eccdc4d30..857426eca 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -175,7 +175,7 @@ for magic values. This is similar to the ``ChoiceFilter``'s null value handling. if value != self.empty_value: return super().filter(qs, value) - qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): ""}) + qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): ""}) return qs.distinct() if self.distinct else qs @@ -199,7 +199,7 @@ behavior as an ``isnull`` filter. exclude = self.exclude ^ (value is False) method = qs.exclude if exclude else qs.filter - return method(**{self.name: ""}) + return method(**{self.field_name: ""}) class MyFilterSet(filters.FilterSet): From 9a11b0bb8c3aaf8275b586458ec7acffcaaea104 Mon Sep 17 00:00:00 2001 From: Phil Date: Thu, 14 Nov 2019 18:20:42 +0100 Subject: [PATCH 058/103] Extend test coverage. (#1130) --- tests/rest_framework/test_backends.py | 34 ++++++++++++++++++++++++++- tests/test_conf.py | 13 ++++++++++ tests/test_filters.py | 5 ++++ tests/test_widgets.py | 28 ++++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/tests/rest_framework/test_backends.py b/tests/rest_framework/test_backends.py index 429ccfc3a..955bdc612 100644 --- a/tests/rest_framework/test_backends.py +++ b/tests/rest_framework/test_backends.py @@ -1,5 +1,5 @@ import warnings -from unittest import skipIf +from unittest import mock, skipIf from django.db.models import BooleanField from django.test import TestCase @@ -407,3 +407,35 @@ class View(generics.ListCreateAPIView): message = str(recorded.pop().message) self.assertEqual(message, expected) self.assertEqual(len(recorded), 0) + + +class DjangoFilterBackendTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + cls.backend = DjangoFilterBackend() + cls.backend.get_filterset_class = lambda x, y: None + + def test_get_filterset_none_filter_class(self): + filterset = self.backend.get_filterset(mock.Mock(), mock.Mock(), mock.Mock()) + self.assertIsNone(filterset) + + def test_filter_queryset_none_filter_class(self): + prev_qs = mock.Mock() + qs = self.backend.filter_queryset(mock.Mock(), prev_qs, mock.Mock()) + self.assertIs(qs, prev_qs) + + def test_to_html_none_filter_class(self): + html = self.backend.to_html(mock.Mock(), mock.Mock(), mock.Mock()) + self.assertIsNone(html) + + def test_get_schema_operation_parameters_userwarning(self): + with self.assertWarns(UserWarning): + view = mock.Mock() + view.__class__.return_value = 'Test' + view.get_queryset.side_effect = Exception + self.backend.get_schema_operation_parameters(view) + + @mock.patch('django_filters.compat.is_crispy', return_value=True) + def test_template_crispy(self, _): + self.assertEqual(self.backend.template, 'django_filters/rest_framework/crispy_form.html') diff --git a/tests/test_conf.py b/tests/test_conf.py index 45a24e826..df1076723 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -1,3 +1,4 @@ +from unittest import mock from django.test import TestCase, override_settings @@ -89,3 +90,15 @@ def method(self): self.assertFalse(is_callable(Class)) self.assertTrue(is_callable(c)) self.assertTrue(is_callable(c.method)) + + +class SettingsObjectTestCase(TestCase): + + @mock.patch('django_filters.conf.DEPRECATED_SETTINGS', ['TEST_123']) + @mock.patch.dict('django_filters.conf.DEFAULTS', {'TEST_123': True}) + def test_get_setting_deprecated(self): + with override_settings(FILTERS_TEST_123=True): + with self.assertWarns(DeprecationWarning): + settings.change_setting('FILTERS_TEST_123', True, True) + test_setting = settings.get_setting('TEST_123') + self.assertTrue(test_setting) diff --git a/tests/test_filters.py b/tests/test_filters.py index 396f47274..d2061f19f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -388,6 +388,11 @@ def test_conjoined_true(self): f = MultipleChoiceFilter(conjoined=True) self.assertTrue(f.conjoined) + def test_is_noop_false(self): + f = MultipleChoiceFilter(required=False) + f.always_filter = False + self.assertFalse(f.is_noop(None, ['value'])) + def test_filtering(self): qs = mock.Mock(spec=['filter']) f = MultipleChoiceFilter(field_name='somefield') diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 2d7928cd1..ec804c1c5 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -185,6 +185,34 @@ class W(SuffixedMultiWidget): }, {}, 'price') self.assertEqual(result, ['1', 'lt']) + def test_value_omitted_from_data(self): + class A(SuffixedMultiWidget): + suffixes = ['b'] + + a = A(widgets=[BooleanWidget]) + + result = a.value_omitted_from_data([], None, 'test') + + self.assertIsNotNone(result) + + def test_replace_name(self): + class A(SuffixedMultiWidget): + suffixes = ['test'] + + a = A(widgets=[None]) + + output = '
' + index = 0 + q = a.replace_name(output, index) + self.assertEqual(q, '
') + + def test_decompress_value_none(self): + class A(SuffixedMultiWidget): + suffixes = [''] + + a = A(widgets=[None]) + self.assertEqual(a.decompress(None), [None, None]) + class RangeWidgetTests(TestCase): From e6d5f2dd0f9e62dea7e7ac5bd933187bcd2e1913 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 2 Dec 2019 20:08:55 +0100 Subject: [PATCH 059/103] Updated tox config for 3.0 final. (#1154) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d27f75615..1ef0cda94 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ deps = django20: django==2.0.* django21: django==2.1.* django22: django==2.2.* - django30: django>=3.0a1,<3.1 + django30: django>=3.0,<3.1 djangorestframework==3.10.* latest: {[latest]deps} -rrequirements/test-ci.txt From 400469f03f7ebac6e0a7d8cbe6a7706f71e14c96 Mon Sep 17 00:00:00 2001 From: Sardorbek Imomaliev Date: Tue, 4 Feb 2020 18:15:24 +0700 Subject: [PATCH 060/103] Provide enum for openapi schema when using ChoiceField (#1168) --- django_filters/rest_framework/backends.py | 16 ++++++++--- tests/rest_framework/models.py | 4 +++ tests/rest_framework/test_backends.py | 34 ++++++++++++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/django_filters/rest_framework/backends.py b/django_filters/rest_framework/backends.py index eed802a41..835a67f9a 100644 --- a/django_filters/rest_framework/backends.py +++ b/django_filters/rest_framework/backends.py @@ -149,8 +149,13 @@ def get_schema_operation_parameters(self, view): ) filterset_class = self.get_filterset_class(view, queryset) - return [] if not filterset_class else [ - ({ + + if not filterset_class: + return [] + + parameters = [] + for field_name, field in filterset_class.base_filters.items(): + parameter = { 'name': field_name, 'required': field.extra['required'], 'in': 'query', @@ -158,5 +163,8 @@ def get_schema_operation_parameters(self, view): 'schema': { 'type': 'string', }, - }) for field_name, field in filterset_class.base_filters.items() - ] + } + if field.extra and 'choices' in field.extra: + parameter['schema']['enum'] = [c[0] for c in field.extra['choices']] + parameters.append(parameter) + return parameters diff --git a/tests/rest_framework/models.py b/tests/rest_framework/models.py index d3a0446f9..a6f5e8a1c 100644 --- a/tests/rest_framework/models.py +++ b/tests/rest_framework/models.py @@ -26,3 +26,7 @@ class DjangoFilterOrderingModel(models.Model): class Meta: ordering = ['-date'] + + +class CategoryItem(BaseFilterableItem): + category = models.CharField(max_length=10, choices=(("home", "Home"), ("office", "Office"))) diff --git a/tests/rest_framework/test_backends.py b/tests/rest_framework/test_backends.py index 955bdc612..bfa97341f 100644 --- a/tests/rest_framework/test_backends.py +++ b/tests/rest_framework/test_backends.py @@ -15,7 +15,7 @@ ) from ..models import Article -from .models import FilterableItem +from .models import CategoryItem, FilterableItem factory = APIRequestFactory() @@ -26,6 +26,12 @@ class Meta: fields = '__all__' +class CategoryItemSerializer(serializers.ModelSerializer): + class Meta: + model = CategoryItem + fields = '__all__' + + # These class are used to test a filter class. class SeveralFieldsFilter(FilterSet): text = filters.CharFilter(lookup_expr='icontains') @@ -52,6 +58,13 @@ class FilterClassRootView(FilterableItemView): filterset_class = SeveralFieldsFilter +class CategoryItemView(generics.ListCreateAPIView): + queryset = CategoryItem.objects.all() + serializer_class = CategoryItemSerializer + filter_backends = (DjangoFilterBackend,) + filterset_fields = ["category"] + + class GetFilterClassTests(TestCase): def test_filterset_class(self): @@ -237,6 +250,25 @@ def test_get_operation_parameters_with_filterset_fields_list(self): self.assertEqual(fields, ['decimal', 'date']) + def test_get_operation_parameters_with_filterset_fields_list_with_choices(self): + backend = DjangoFilterBackend() + fields = backend.get_schema_operation_parameters(CategoryItemView()) + + self.assertEqual( + fields, + [{ + 'name': 'category', + 'required': False, + 'in': 'query', + 'description': 'category', + 'schema': { + 'type': 'string', + 'enum': ['home', 'office'] + }, + + }] + ) + class TemplateTests(TestCase): def test_backend_output(self): From e71d23536c4e0ebc09dd06d3d2c943e8169a5b91 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 4 Mar 2020 09:02:01 -0800 Subject: [PATCH 061/103] Fix CSVWidget attrs handling (#1177) * Test BaseCSVWidget explicitly * Add attrs to CSV widget tests * Fix BaseCSVWidget test widget * Fix CSVWidget attrs when rendering multiple values - BaseCSVWidget creates surrogate on initialization - CSVWidget copies attrs to surrogate --- django_filters/widgets.py | 23 +++++++++++--- tests/test_widgets.py | 65 +++++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/django_filters/widgets.py b/django_filters/widgets.py index b22ceaeab..889fd812b 100644 --- a/django_filters/widgets.py +++ b/django_filters/widgets.py @@ -1,4 +1,5 @@ from collections.abc import Iterable +from copy import deepcopy from itertools import chain from re import search, sub @@ -189,6 +190,17 @@ def value_from_datadict(self, data, files, name): class BaseCSVWidget(forms.Widget): + # Surrogate widget for rendering multiple values + surrogate = forms.TextInput + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if isinstance(self.surrogate, type): + self.surrogate = self.surrogate() + else: + self.surrogate = deepcopy(self.surrogate) + def _isiterable(self, value): return isinstance(value, Iterable) and not isinstance(value, str) @@ -212,15 +224,18 @@ def render(self, name, value, attrs=None, renderer=None): # if we have multiple values, we need to force render as a text input # (otherwise, the additional values are lost) - surrogate = forms.TextInput() - value = [force_str(surrogate.format_value(v)) for v in value] + value = [force_str(self.surrogate.format_value(v)) for v in value] value = ','.join(list(value)) - return surrogate.render(name, value, attrs, renderer=renderer) + return self.surrogate.render(name, value, attrs, renderer=renderer) class CSVWidget(BaseCSVWidget, forms.TextInput): - pass + def __init__(self, *args, attrs=None, **kwargs): + super().__init__(*args, attrs, **kwargs) + + if attrs is not None: + self.surrogate.attrs.update(attrs) class QueryArrayWidget(BaseCSVWidget, forms.TextInput): diff --git a/tests/test_widgets.py b/tests/test_widgets.py index ec804c1c5..e011756a6 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -1,4 +1,4 @@ -from django.forms import Select, TextInput +from django.forms import NumberInput, Select, TextInput from django.test import TestCase from django_filters.widgets import ( @@ -266,23 +266,26 @@ def test_widget_value_from_datadict(self): self.assertEqual(result, None) -class CSVWidgetTests(TestCase): - def test_widget(self): - w = CSVWidget() +class BaseCSVWidgetTests(TestCase): + def test_widget_render(self): + class NumberCSVWidget(BaseCSVWidget, NumberInput): + pass + + w = NumberCSVWidget(attrs={'test': 'attr'}) self.assertHTMLEqual(w.render('price', None), """ - """) + """) self.assertHTMLEqual(w.render('price', ''), """ - """) + """) self.assertHTMLEqual(w.render('price', []), """ - """) + """) self.assertHTMLEqual(w.render('price', '1'), """ - """) + """) self.assertHTMLEqual(w.render('price', '1,2'), """ - """) + """) self.assertHTMLEqual(w.render('price', ['1', '2']), """ """) @@ -291,8 +294,10 @@ def test_widget(self): """) def test_widget_value_from_datadict(self): - w = CSVWidget() + class NumberCSVWidget(BaseCSVWidget, NumberInput): + pass + w = NumberCSVWidget() data = {'price': None} result = w.value_from_datadict(data, {}, 'price') self.assertEqual(result, None) @@ -324,6 +329,46 @@ def test_widget_value_from_datadict(self): result = w.value_from_datadict({}, {}, 'price') self.assertEqual(result, None) + def test_surrogate_class(self): + class ClassSurrogate(BaseCSVWidget, NumberInput): + surrogate = NumberInput + + w = ClassSurrogate() + self.assertIsInstance(w.surrogate, NumberInput) + + def test_surrogate_instance(self): + class InstanceSurrogate(BaseCSVWidget, NumberInput): + surrogate = NumberInput() + + w = InstanceSurrogate() + self.assertIsInstance(w.surrogate, NumberInput) + self.assertIsNot(InstanceSurrogate.surrogate, w.surrogate) # deepcopied + + +class CSVWidgetTests(TestCase): + def test_widget_render(self): + w = CSVWidget(attrs={'test': 'attr'}) + self.assertHTMLEqual(w.render('price', None), """ + """) + + self.assertHTMLEqual(w.render('price', ''), """ + """) + + self.assertHTMLEqual(w.render('price', []), """ + """) + + self.assertHTMLEqual(w.render('price', '1'), """ + """) + + self.assertHTMLEqual(w.render('price', '1,2'), """ + """) + + self.assertHTMLEqual(w.render('price', ['1', '2']), """ + """) + + self.assertHTMLEqual(w.render('price', [1, 2]), """ + """) + class CSVSelectTests(TestCase): class CSVSelect(BaseCSVWidget, Select): From 873786e30a8d69e2b00db31fcd70c93b8b33debb Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 4 Mar 2020 09:15:01 -0800 Subject: [PATCH 062/103] Update CI (#1178) * Using tox-venv no longer necessary VirtualEnv rewrite should fix compatibility issues * Update travis conf - Set os option - Use Ubuntu bionic - Remove deprecated sudo flag - Update 'matrix' to 'jobs' * Add Python 3.8 to Azure config * Use tox-factor for tox test env selection This should fix Azure builds so they don't run all tox test envs. * Fixup tox Python env factor --- .azure_pipelines/azure-pipelines.yml | 30 ++++++++++++++++------------ .travis.yml | 19 +++++++++--------- tox.ini | 4 ++-- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/.azure_pipelines/azure-pipelines.yml b/.azure_pipelines/azure-pipelines.yml index ca7126c40..0189bf0de 100644 --- a/.azure_pipelines/azure-pipelines.yml +++ b/.azure_pipelines/azure-pipelines.yml @@ -4,22 +4,25 @@ strategy: matrix: - # For some reason, Python 3.4 tries to install Django 2.0 - disabling for now - # PY34: - # PYTHON_VERSION: '3.4' PY35: PYTHON_VERSION: '3.5' - TOXENV: django111, django20, django21 + TOXFACTOR: py35 PY36: PYTHON_VERSION: '3.6' - PY36_isort: - PYTHON_VERSION: '3.6' - TOXENV: isort,lint,docs - PY36_warnings: - PYTHON_VERSION: '3.6' - TOXENV: warnings + TOXFACTOR: py36 PY37: PYTHON_VERSION: '3.7' + TOXFACTOR: py37 + PY38: + PYTHON_VERSION: '3.8' + TOXFACTOR: py38 + + PY38_isort: + PYTHON_VERSION: '3.8' + TOXENV: isort,lint,docs + PY38_warnings: + PYTHON_VERSION: '3.8' + TOXENV: warnings steps: - task: UsePythonVersion@0 @@ -32,12 +35,13 @@ steps: displayName: Ensure latest setuptools - script: | - pip install coverage tox tox-venv unittest-xml-reporting + pip install coverage tox tox-factor unittest-xml-reporting displayName: Install deps - script: | pip --version - tox --skip-missing-interpreters true + tox --version + tox displayName: Run tox - script: | @@ -67,4 +71,4 @@ steps: pip install codecov codecov displayName: Codecov - condition: false \ No newline at end of file + condition: false diff --git a/.travis.yml b/.travis.yml index 7dac3eba1..1740ac1e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,24 @@ -sudo: false - -dist: xenial +os: linux +dist: bionic language: python -python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" cache: pip install: - - pip install coverage tox tox-travis tox-venv + - pip install coverage tox tox-factor script: - coverage erase - tox -matrix: +jobs: fast_finish: true include: + - { python: "3.5", env: TOXFACTOR=py35 } + - { python: "3.6", env: TOXFACTOR=py36 } + - { python: "3.7", env: TOXFACTOR=py37 } + - { python: "3.8", env: TOXFACTOR=py38 } + - python: "3.8" env: TOXENV=isort,lint,docs - python: "3.8" diff --git a/tox.ini b/tox.ini index 1ef0cda94..02fc9b236 100644 --- a/tox.ini +++ b/tox.ini @@ -4,8 +4,8 @@ envlist = {py35,py36}-django20, {py35,py36,py37}-django21, {py35,py36,py37}-django22, - {py36,py37,38}-django30, - {py36,py37,38}-latest, + {py36,py37,py38}-django30, + {py36,py37,py38}-latest, isort,lint,docs,warnings, From df753fdfbce783b22ec6855ecd4d4d3b2ea346a6 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Wed, 6 Mar 2019 20:08:33 +0600 Subject: [PATCH 063/103] Removed unneeded models.py file. --- django_filters/models.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 django_filters/models.py diff --git a/django_filters/models.py b/django_filters/models.py deleted file mode 100644 index e69de29bb..000000000 From 754146fec4747d6228d15b6ed7ce8d9db9f7bb3e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 19:56:14 +0100 Subject: [PATCH 064/103] Add Bulgarian translations (#1160) --- .../locale/bg/LC_MESSAGES/django.mo | Bin 0 -> 2773 bytes .../locale/bg/LC_MESSAGES/django.po | 188 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 django_filters/locale/bg/LC_MESSAGES/django.mo create mode 100644 django_filters/locale/bg/LC_MESSAGES/django.po diff --git a/django_filters/locale/bg/LC_MESSAGES/django.mo b/django_filters/locale/bg/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..0f3ede44463a28c2317ac341375b719c8add43af GIT binary patch literal 2773 zcmZvc+iw(A9LEm=URJz-7x2nK1zVb_y9K2emWx0M#!7<=@y5e+_jGsY?94JV+ja>t z&?a6&Z4<=!VAK$efwxjxy7U?!yuFY!iHY%rKS1B$$;1c0-`Q!Q;ACb#bAIRi?!VpZ zYgT+AaBar(EFOEA5N+VhJ$T`|uv~~oz>6SmaTTxo!CCMj@Mo|A{2h!`R@C;FgAYQ! z3S1Aa1MdZ!!34Myd;oj{TnT2t`@r`=m=ay_em6*eB@nl$;Klt9LFx~H)Efd4& zd60S+LEK^tFXk~B^B3?Ltp5P%|0c+I{{d-dC4}q1H6ZhB02zM^h+FK!i}u@N`N5b+ zK-xJP?^_^D3J?4dwe16+#<~ST(_RLA416Da9(2JEz~OlRZ}4HPZ-CE%%iwfBxE@>s z=0V1J0(>0&9OU^6;A(IZr2pSQ*8LjzDEKFcTl^LCCdjz{1-ZW##g$+k$oLL`Pk={3 z#_fY^!4vWRS&;Gn0CEm~0(t(|c>j95e*@eL`Dz5iIv)WUR~9e&cjEP@AoDl}GOzDI z+N*-}e+@)AqZvSH7O#zqU$wd8tgt?uZPshyqKy~v$XM_K$UZX$#?RT{#oXza7wgHK zSs(Ve9uKdl@Gy7I7i-Es)#2HM2Vq7RbJ&cBIWsTzgx7`|#7i-sj7hv4e`92X*yzi; zobt2E$yrXmUhKAQvD>N0oEa$5ZYet_yDd9Vo-aDec3_okC3{V~tbAECE3!*TUzJP` zr*pEaBC~F>X!?!fEms`$a>|2E(P{dD+y;vkUmP->ypqSu%Bx7#UsBn?mqDK!n`{(s zm%EBqATk9D_FX3^)IKZ9^hCym331rzaoj#fysLaZh>X%K&YG6vizseNkdQC?ELc$e zS-YH5qG0;cL~1<>fpCG31w!DKJpr$=r6?V)^PWO-o(u}6bLU#Ro>a%nrY!@PdscM9 za-_#JDY2E01Y_03#S*=u8DtA6r>F9&Ulc8;jLO$?cYBp*nE5M56Af$5=c zQF47s^{})eDp9BB9z#uy13BXeT4uRUtIQokFs@PSV&af0xn5v&_<1X5yk5@xM#gQG ziOz!=V-GWhim?|7x5}nus@X`UjHazJm1<3HX-K}BOyZz%NcCDYee2w7MzY0dYT>!9 z+ZvKvaPCbs-N>L0zHJ7s*D5=#o`PxFVBopkrsMX??Zx6QWvgtXSKa{yCto)6%E&0Q zh=>-}tkl%bsZ)?XFk!TcuNqmVSCmo2-4N>zs|LWICOan>I;)CO4#0 zDY;W7C5~ggBi*#HwLQI+pOHp#dnzeUo~)_hOtKz2Dd;rSw&_A5u>N5jH&x+-S{ zGS%4J*pi5Tk9uCu$#76#3eV^gjMO90*T^XJll6FUUspjL=`}3FuBmf+14sNthiCKh_g8udhlygWb#FU^pP@7?y@) zcnaPp;blTk^B_%6LNp(aVE+tMs*N?{144hR=iqzz;zX0sb zLU2{+FC!~FahlR9myxeIh9zO*3WP`xfg_-C*5Qtr!x8xY2IWWtwFKEYTG8p;e7Kh$I+=fCk$z9S&m)9fW`p){=xYYWaj*JW^e9AXfZ8nXZb+ z;_15rq+hAW1V)DAFvONfro*|oU4~Gmb+y*>v0CoX8oRqa6C#QksV=d_JQiek+luOj zj-VX1V&DrS&hDbF=m8cOR(zxCdL|r1%$PzB;1wF?Pk?@_@PQd+Q*LYReAJK$J;m`~ nT5F<&DhWY literal 0 HcmV?d00001 diff --git a/django_filters/locale/bg/LC_MESSAGES/django.po b/django_filters/locale/bg/LC_MESSAGES/django.po new file mode 100644 index 000000000..227f66c8f --- /dev/null +++ b/django_filters/locale/bg/LC_MESSAGES/django.po @@ -0,0 +1,188 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Hristo Gatsinski , 2019. +# +#: conf.py:27 conf.py:28 conf.py:41 +msgid "" +msgstr "" +"Project-Id-Version: django-filter\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-12-22 19:45+0200\n" +"PO-Revision-Date: 2019-12-21 19:36+0200\n" +"Last-Translator: Hristo Gatsinski \n" +"Language-Team: \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 1.8.9\n" + +#: conf.py:17 +msgid "date" +msgstr "дата" + +#: conf.py:18 +msgid "year" +msgstr "година" + +#: conf.py:19 +msgid "month" +msgstr "месец" + +#: conf.py:20 +msgid "day" +msgstr "ден" + +#: conf.py:21 +msgid "week day" +msgstr "ден от седмицата" + +#: conf.py:22 +msgid "hour" +msgstr "час" + +#: conf.py:23 +msgid "minute" +msgstr "минута" + +#: conf.py:24 +msgid "second" +msgstr "секунда" + +#: conf.py:29 conf.py:30 +msgid "contains" +msgstr "съдържа" + +#: conf.py:31 +msgid "is in" +msgstr "в" + +#: conf.py:32 +msgid "is greater than" +msgstr "е по-голям от" + +#: conf.py:33 +msgid "is greater than or equal to" +msgstr "е по-голям или равен на" + +#: conf.py:34 +msgid "is less than" +msgstr "е по-малък от" + +#: conf.py:35 +msgid "is less than or equal to" +msgstr "е по-малък или равен на" + +#: conf.py:36 conf.py:37 +msgid "starts with" +msgstr "започва с" + +#: conf.py:38 conf.py:39 +msgid "ends with" +msgstr "завършва с" + +#: conf.py:40 +msgid "is in range" +msgstr "е в диапазона" + +#: conf.py:42 conf.py:43 +msgid "matches regex" +msgstr "съвпада с регуларен израз" + +#: conf.py:44 conf.py:52 +msgid "search" +msgstr "търсене" + +#: conf.py:47 +msgid "is contained by" +msgstr "се съдържа от" + +#: conf.py:48 +msgid "overlaps" +msgstr "припокрива" + +#: conf.py:49 +msgid "has key" +msgstr "има ключ" + +#: conf.py:50 +msgid "has keys" +msgstr "има ключове" + +#: conf.py:51 +msgid "has any keys" +msgstr "има който и да е ключ" + +#: fields.py:106 +msgid "Select a lookup." +msgstr "Изберете справка" + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "Търсенето по диапазон изисква две стойности" + +#: filters.py:406 +msgid "Today" +msgstr "Днес" + +#: filters.py:407 +msgid "Yesterday" +msgstr "Вчера" + +#: filters.py:408 +msgid "Past 7 days" +msgstr "Последните 7 дни" + +#: filters.py:409 +msgid "This month" +msgstr "Този месец" + +#: filters.py:410 +msgid "This year" +msgstr "Тази година" + +#: filters.py:508 +msgid "Multiple values may be separated by commas." +msgstr "Множество стойности може да се разделят със запетая" + +#: filters.py:681 +#, python-format +msgid "%s (descending)" +msgstr "%s (намалавящ)" + +#: filters.py:697 +msgid "Ordering" +msgstr "Подредба" + +#: rest_framework/filterset.py:31 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Изпращане" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Филтри на полетата" + +#: utils.py:298 +msgid "exclude" +msgstr "изключва" + +#: widgets.py:57 +msgid "All" +msgstr "Всичко" + +#: widgets.py:159 +msgid "Unknown" +msgstr "Неизвестен" + +#: widgets.py:160 +msgid "Yes" +msgstr "Да" + +#: widgets.py:161 +msgid "No" +msgstr "Не" From d0a152264b582fb91c61806579c89ea5f9d7708c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 20:07:15 +0100 Subject: [PATCH 065/103] Add relative time example to docs (#1059) --- docs/guide/tips.txt | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index 857426eca..0a436641f 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -209,6 +209,35 @@ behavior as an ``isnull`` filter. model = MyModel +Filtering by relative times +--------------------------- + +Given a model with a timestamp field, it may be useful to filter based on relative times. +For instance, perhaps we want to get data from the past *n* hours. +This could be accomplished the with a ``NumberFilter`` that invokes a custom method. + +.. code-block:: python + + from django.utils import timezone + from datetime import timedelta + ... + + class DataModel(models.Model): + time_stamp = models.DateTimeField() + + + class DataFilter(django_filters.FilterSet): + hours = django_filters.NumberFilter( + field_name='time_stamp', method='get_past_n_hours', label="Past n hours") + + def get_past_n_hours(self, queryset, field_name, value): + time_threshold = timezone.now() - timedelta(hours=int(value)) + return queryset.filter(time_stamp__gte=time_threshold) + + class Meta: + model = DataModel + fields = ('hours',) + Using ``initial`` values as defaults ------------------------------------ From e072c5cdaba8b126ee4999d306a4ff0652301061 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 20:09:10 +0100 Subject: [PATCH 066/103] Removed empty action from a form example. (#1066) https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-action >The action and formaction content attributes, if specified, must have a value that is a valid non-empty URL potentially surrounded by spaces. --- docs/guide/usage.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 68fb21a96..b7ec498f5 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -280,7 +280,7 @@ And lastly we need a template:: {% extends "base.html" %} {% block content %} -
+ {{ filter.form.as_p }}
From 53fca29ce60eae1e5e686e6f6b41aaca5b6faf8d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 20:17:05 +0100 Subject: [PATCH 067/103] Added example of passing help_text to filters. Closes #1071. Thanks to @rpkilby of the suggestion. --- docs/guide/tips.txt | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index 0a436641f..9997aecaf 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -214,7 +214,7 @@ Filtering by relative times Given a model with a timestamp field, it may be useful to filter based on relative times. For instance, perhaps we want to get data from the past *n* hours. -This could be accomplished the with a ``NumberFilter`` that invokes a custom method. +This could be accomplished the with a ``NumberFilter`` that invokes a custom method. .. code-block:: python @@ -224,8 +224,8 @@ This could be accomplished the with a ``NumberFilter`` that invokes a custom met class DataModel(models.Model): time_stamp = models.DateTimeField() - - + + class DataFilter(django_filters.FilterSet): hours = django_filters.NumberFilter( field_name='time_stamp', method='get_past_n_hours', label="Past n hours") @@ -273,3 +273,17 @@ If defaults are necessary though, the following should mimic the pre-1.0 behavio data[name] = initial super().__init__(data, *args, **kwargs) + + +Adding model field ``help_text`` to filters +------------------------------------------- + +Model field ``help_text`` is not used by filters by default. It can be added +using a simple FilterSet base class:: + + class HelpfulFilterSet(django_filters.FilterSet): + @classmethod + def filter_for_field(cls, f, name, lookup_expr): + filter = super(HelpfulFilterSet, cls).filter_for_field(f, name, lookup_expr) + filter.extra['help_text'] = f.help_text + return filter From 62e621d64c1cefe2f2ebda806674777e5587a09e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 20:18:11 +0100 Subject: [PATCH 068/103] Raise error for non-model fields in Meta.fields. (#1061) --- django_filters/filterset.py | 10 +++-- docs/ref/filterset.txt | 19 +++++++++ tests/rest_framework/test_backends.py | 4 +- tests/test_filterset.py | 59 +++++++++++++++++---------- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/django_filters/filterset.py b/django_filters/filterset.py index a594a1229..13460e580 100644 --- a/django_filters/filterset.py +++ b/django_filters/filterset.py @@ -344,12 +344,14 @@ def get_filters(cls): if field is not None: filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr) - # filter out declared filters - undefined = [f for f in undefined if f not in cls.declared_filters] + # Allow Meta.fields to contain declared filters *only* when a list/tuple + if isinstance(cls._meta.fields, (list, tuple)): + undefined = [f for f in undefined if f not in cls.declared_filters] + if undefined: raise TypeError( - "'Meta.fields' contains fields that are not defined on this FilterSet: " - "%s" % ', '.join(undefined) + "'Meta.fields' must not contain non-model field names: %s" + % ', '.join(undefined) ) # Add in declared filters. This is necessary since we don't enforce adding diff --git a/docs/ref/filterset.txt b/docs/ref/filterset.txt index 9100c0106..202ebf8e4 100644 --- a/docs/ref/filterset.txt +++ b/docs/ref/filterset.txt @@ -71,6 +71,25 @@ include both transforms and lookups, as detailed in the `lookup reference`__. __ https://docs.djangoproject.com/en/stable/ref/models/lookups/#module-django.db.models.lookups +Note that it is **not** necessary to included declared filters in a ``fields`` +list - doing so will have no effect - and including declarative aliases in a +``fields`` dict will raise an error. + +.. code-block:: python + + class UserFilter(django_filters.FilterSet): + username = filters.CharFilter() + login_timestamp = filters.IsoDateTimeFilter(field_name='last_login') + + class Meta: + model = User + fields = { + 'username': ['exact', 'contains'], + 'login_timestamp': ['exact'], + } + + TypeError("'Meta.fields' contains fields that are not defined on this FilterSet: login_timestamp") + .. _exclude: diff --git a/tests/rest_framework/test_backends.py b/tests/rest_framework/test_backends.py index bfa97341f..66fecf490 100644 --- a/tests/rest_framework/test_backends.py +++ b/tests/rest_framework/test_backends.py @@ -121,7 +121,7 @@ def test_filterset_fields_malformed(self): view.filterset_fields = ['non_existent'] queryset = FilterableItem.objects.all() - msg = "'Meta.fields' contains fields that are not defined on this FilterSet: non_existent" + msg = "'Meta.fields' must not contain non-model field names: non_existent" with self.assertRaisesMessage(TypeError, msg): backend.get_filterset_class(view, queryset) @@ -173,7 +173,7 @@ class View(FilterFieldsRootView): backend = DjangoFilterBackend() - msg = "'Meta.fields' contains fields that are not defined on this FilterSet: non_existent" + msg = "'Meta.fields' must not contain non-model field names: non_existent" with self.assertRaisesMessage(TypeError, msg): backend.get_schema_fields(View()) diff --git a/tests/test_filterset.py b/tests/test_filterset.py index 23460eca6..8fa7396f1 100644 --- a/tests/test_filterset.py +++ b/tests/test_filterset.py @@ -429,10 +429,11 @@ class F(FilterSet): username = CharFilter() class Meta: - model = Book - fields = {'id': ['exact'], - 'username': ['exact'], - } + model = User + fields = { + 'id': ['exact'], + 'username': ['exact'], + } self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 2) @@ -440,8 +441,11 @@ class Meta: expected_list = ['id', 'username'] self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list)) - def test_meta_fields_containing_unknown(self): - with self.assertRaises(TypeError) as excinfo: + def test_meta_fields_list_containing_unknown_fields(self): + msg = ("'Meta.fields' must not contain non-model field names: " + "other, another") + + with self.assertRaisesMessage(TypeError, msg): class F(FilterSet): username = CharFilter() @@ -449,36 +453,47 @@ class Meta: model = Book fields = ('username', 'price', 'other', 'another') - self.assertEqual( - str(excinfo.exception), - "'Meta.fields' contains fields that are not defined on this FilterSet: " - "other, another" - ) + def test_meta_fields_dict_containing_unknown_fields(self): + msg = "'Meta.fields' must not contain non-model field names: other" + + with self.assertRaisesMessage(TypeError, msg): + class F(FilterSet): + + class Meta: + model = Book + fields = { + 'id': ['exact'], + 'title': ['exact'], + 'other': ['exact'], + } - def test_meta_fields_dictionary_containing_unknown(self): - with self.assertRaises(TypeError): + def test_meta_fields_dict_containing_declarative_alias(self): + # Meta.fields dict cannot generate lookups for an *aliased* field + msg = "'Meta.fields' must not contain non-model field names: other" + + with self.assertRaisesMessage(TypeError, msg): class F(FilterSet): + other = CharFilter() class Meta: model = Book - fields = {'id': ['exact'], - 'title': ['exact'], - 'other': ['exact'], - } + fields = { + 'id': ['exact'], + 'title': ['exact'], + 'other': ['exact'], + } def test_meta_fields_invalid_lookup(self): # We want to ensure that non existent lookups (or just simple misspellings) # throw a useful exception containg the field and lookup expr. - with self.assertRaises(FieldLookupError) as context: + msg = "Unsupported lookup 'flub' for field 'tests.User.username'." + + with self.assertRaisesMessage(FieldLookupError, msg): class F(FilterSet): class Meta: model = User fields = {'username': ['flub']} - exc = str(context.exception) - self.assertIn('tests.User.username', exc) - self.assertIn('flub', exc) - def test_meta_exlude_with_declared_and_declared_wins(self): class F(FilterSet): username = CharFilter() From bdb190b29cf4a1462da3ccf9fa7620596aeb777f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 20:21:11 +0100 Subject: [PATCH 069/103] Added ``required`` argument to filter reference. (#1098) Fix #330 --- docs/ref/filters.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index e534a896f..06319111a 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -108,6 +108,11 @@ span relationships. Defaults to ``False``. A boolean that specifies whether the Filter should use ``filter`` or ``exclude`` on the queryset. Defaults to ``False``. +``required`` +~~~~~~~~~~~~ + +A boolean that specifies whether the Filter is required or not. Defaults to ``False``. + ``**kwargs`` ~~~~~~~~~~~~ From 3ee468b153d16d1ee96c1614444e3ad11faf298a Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 20:51:17 +0100 Subject: [PATCH 070/103] Add missing Meta.fields in docs example. (#1155) This example doesn't work as is, since a FilterSet with `Meta.model` requires `Meta.fields` (or `meta.exclude`). --- docs/guide/tips.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index 9997aecaf..0c82e6606 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -207,6 +207,7 @@ behavior as an ``isnull`` filter. class Meta: model = MyModel + fields = [] Filtering by relative times From 6251bb5292341583f14ec5e3069754882557b07a Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 20:52:32 +0100 Subject: [PATCH 071/103] Fix filterset multiple inheritance bug (#1131) * Fix filterset multiple inheritance bug * Ensure filter order under multiple inheritance --- django_filters/filterset.py | 26 ++++++++++++++--------- tests/test_filterset.py | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/django_filters/filterset.py b/django_filters/filterset.py index 13460e580..0418adf2e 100644 --- a/django_filters/filterset.py +++ b/django_filters/filterset.py @@ -95,16 +95,22 @@ def get_declared_filters(cls, bases, attrs): filters.sort(key=lambda x: x[1].creation_counter) - # merge declared filters from base classes - for base in reversed(bases): - if hasattr(base, 'declared_filters'): - filters = [ - (name, f) for name, f - in base.declared_filters.items() - if name not in attrs - ] + filters - - return OrderedDict(filters) + # Ensures a base class field doesn't override cls attrs, and maintains + # field precedence when inheriting multiple parents. e.g. if there is a + # class C(A, B), and A and B both define 'field', use 'field' from A. + known = set(attrs) + + def visit(name): + known.add(name) + return name + + base_filters = [ + (visit(name), f) + for base in bases if hasattr(base, 'declared_filters') + for name, f in base.declared_filters.items() if name not in known + ] + + return OrderedDict(base_filters + filters) FILTER_FOR_DBFIELD_DEFAULTS = { diff --git a/tests/test_filterset.py b/tests/test_filterset.py index 8fa7396f1..6930ce492 100644 --- a/tests/test_filterset.py +++ b/tests/test_filterset.py @@ -634,6 +634,48 @@ class Grandchild(Child): self.assertEqual(len(Child.base_filters), 1) self.assertEqual(len(Grandchild.base_filters), 1) + def test_declared_filter_multiple_inheritance(self): + class A(FilterSet): + f = CharFilter() + + class B(FilterSet): + f = NumberFilter() + + class F(A, B): + pass + + filters = {name: type(f) for name, f in F.declared_filters.items()} + self.assertEqual(filters, {'f': CharFilter}) + + def test_declared_filter_multiple_inheritance_field_ordering(self): + class Base(FilterSet): + f1 = CharFilter() + f2 = CharFilter() + + class A(Base): + f3 = NumberFilter() + + class B(FilterSet): + f3 = CharFilter() + f4 = CharFilter() + + class F(A, B): + f2 = NumberFilter() + f5 = CharFilter() + + fields = {name: type(f) for name, f in F.declared_filters.items()} + + # `NumberFilter`s should be the 'winners' in filter name conflicts + # - `F.f2` should override `Base.F2` + # - `A.f3` should override `B.f3` + assert fields == { + 'f1': CharFilter, + 'f2': NumberFilter, + 'f3': NumberFilter, + 'f4': CharFilter, + 'f5': CharFilter, + } + class FilterSetInstantiationTests(TestCase): From fbb67b6d8d8a8114c69c16b8eaba81cea68e839e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 4 Mar 2020 21:07:11 +0100 Subject: [PATCH 072/103] Allowed customising default lookup expression. (#1129) --- django_filters/conf.py | 2 ++ django_filters/filters.py | 12 +++----- django_filters/filterset.py | 12 ++++---- docs/guide/usage.txt | 3 +- docs/ref/settings.txt | 8 ++++++ tests/test_conf.py | 3 ++ tests/test_filters.py | 13 --------- tests/test_filterset.py | 55 +++++++++++++++++++++++++++++++++++-- 8 files changed, 79 insertions(+), 29 deletions(-) diff --git a/django_filters/conf.py b/django_filters/conf.py index 12bd4b229..41a6bc533 100644 --- a/django_filters/conf.py +++ b/django_filters/conf.py @@ -7,6 +7,8 @@ DEFAULTS = { 'DISABLE_HELP_TEXT': False, + 'DEFAULT_LOOKUP_EXPR': 'exact', + # empty/null choices 'EMPTY_CHOICE_LABEL': '---------', 'NULL_CHOICE_LABEL': None, diff --git a/django_filters/filters.py b/django_filters/filters.py index 7331bfd1d..0017291ce 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -66,8 +66,10 @@ class Filter(object): creation_counter = 0 field_class = forms.Field - def __init__(self, field_name=None, lookup_expr='exact', *, label=None, + def __init__(self, field_name=None, lookup_expr=None, *, label=None, method=None, distinct=False, exclude=False, **kwargs): + if lookup_expr is None: + lookup_expr = settings.DEFAULT_LOOKUP_EXPR self.field_name = field_name self.lookup_expr = lookup_expr self.label = label @@ -81,12 +83,6 @@ def __init__(self, field_name=None, lookup_expr='exact', *, label=None, self.creation_counter = Filter.creation_counter Filter.creation_counter += 1 - # TODO: remove assertion in 2.1 - assert not isinstance(self.lookup_expr, (type(None), list)), \ - "The `lookup_expr` argument no longer accepts `None` or a list of " \ - "expressions. Use the `LookupChoiceFilter` instead. See: " \ - "https://django-filter.readthedocs.io/en/master/guide/migration.html" - def get_method(self, qs): """Return filter method based on whether we're excluding or simply filtering. @@ -254,7 +250,7 @@ def filter(self, qs, value): def get_filter_predicate(self, v): name = self.field_name - if name and self.lookup_expr != 'exact': + if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR: name = LOOKUP_SEP.join([name, self.lookup_expr]) try: return {name: getattr(v, self.field.to_field_name)} diff --git a/django_filters/filterset.py b/django_filters/filterset.py index 0418adf2e..d174718c0 100644 --- a/django_filters/filterset.py +++ b/django_filters/filterset.py @@ -294,7 +294,7 @@ def get_fields(cls): # Remove excluded fields exclude = exclude or [] if not isinstance(fields, dict): - fields = [(f, ['exact']) for f in fields if f not in exclude] + fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude] else: fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude] @@ -310,9 +310,9 @@ def get_filter_name(cls, field_name, lookup_expr): filter_name = LOOKUP_SEP.join([field_name, lookup_expr]) # This also works with transformed exact lookups, such as 'date__exact' - _exact = LOOKUP_SEP + 'exact' - if filter_name.endswith(_exact): - filter_name = filter_name[:-len(_exact)] + _default_expr = LOOKUP_SEP + settings.DEFAULT_LOOKUP_EXPR + if filter_name.endswith(_default_expr): + filter_name = filter_name[:-len(_default_expr)] return filter_name @@ -366,7 +366,9 @@ def get_filters(cls): return filters @classmethod - def filter_for_field(cls, field, field_name, lookup_expr='exact'): + def filter_for_field(cls, field, field_name, lookup_expr=None): + if lookup_expr is None: + lookup_expr = settings.DEFAULT_LOOKUP_EXPR field, lookup_type = resolve_field(field, lookup_expr) default = { diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index b7ec498f5..7f29cf185 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -122,7 +122,8 @@ The above would generate 'price__lt', 'price__gt', 'release_date', and The filter lookup type 'exact' is an implicit default and therefore never added to a filter name. In the above example, the release date's exact - filter is 'release_date', not 'release_date__exact'. + filter is 'release_date', not 'release_date__exact'. This can be overridden + by the FILTERS_DEFAULT_LOOKUP_EXPR setting. Items in the ``fields`` sequence in the ``Meta`` class may include "relationship paths" using Django's ``__`` syntax to filter on fields on a diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 856b35739..0186498cf 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -7,6 +7,14 @@ default values. All settings are prefixed with ``FILTERS_``, although this is a bit verbose it helps to make it easy to identify these settings. +FILTERS_DEFAULT_LOOKUP_EXPR +--------------------------- + +Default: ``'exact'`` + +Set the default lookup expression to be generated, when none is defined. + + FILTERS_EMPTY_CHOICE_LABEL -------------------------- diff --git a/tests/test_conf.py b/tests/test_conf.py index df1076723..e3f9d254c 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -11,6 +11,9 @@ def test_verbose_lookups(self): self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict) self.assertIn('exact', settings.VERBOSE_LOOKUPS) + def test_default_lookup_expr(self): + self.assertEqual(settings.DEFAULT_LOOKUP_EXPR, 'exact') + def test_disable_help_text(self): self.assertFalse(settings.DISABLE_HELP_TEXT) diff --git a/tests/test_filters.py b/tests/test_filters.py index d2061f19f..a46f7567e 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -94,19 +94,6 @@ def test_field_with_single_lookup_expr(self): field = f.field self.assertIsInstance(field, forms.Field) - def test_field_with_lookup_types_removal(self): - msg = ( - "The `lookup_expr` argument no longer accepts `None` or a list of " - "expressions. Use the `LookupChoiceFilter` instead. See: " - "https://django-filter.readthedocs.io/en/master/guide/migration.html" - ) - - with self.assertRaisesMessage(AssertionError, msg): - Filter(lookup_expr=[]) - - with self.assertRaisesMessage(AssertionError, msg): - Filter(lookup_expr=None) - def test_field_params(self): with mock.patch.object(Filter, 'field_class', spec=['__call__']) as mocked: diff --git a/tests/test_filterset.py b/tests/test_filterset.py index 6930ce492..4ccf8fd25 100644 --- a/tests/test_filterset.py +++ b/tests/test_filterset.py @@ -2,7 +2,7 @@ import unittest from django.db import models -from django.test import TestCase +from django.test import TestCase, override_settings from django_filters.exceptions import FieldLookupError from django_filters.filters import ( @@ -198,6 +198,13 @@ def test_transformed_lookup_expr(self): self.assertIsInstance(result, NumberFilter) self.assertEqual(result.field_name, 'date') + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains') + def test_modified_default_lookup(self): + f = User._meta.get_field('username') + result = FilterSet.filter_for_field(f, 'username') + self.assertIsInstance(result, CharFilter) + self.assertEqual(result.lookup_expr, 'icontains') + @unittest.skip('todo') def test_filter_overrides(self): pass @@ -330,6 +337,13 @@ class F(FilterSet): self.assertEqual(len(F.base_filters), 1) self.assertListEqual(list(F.base_filters), ['username']) + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains') + def test_declaring_filter_other_default_lookup(self): + class F(FilterSet): + username = CharFilter() + + self.assertEqual(F.base_filters['username'].lookup_expr, 'icontains') + def test_model_derived(self): class F(FilterSet): class Meta: @@ -341,6 +355,16 @@ class Meta: self.assertListEqual(list(F.base_filters), ['title', 'price', 'average_rating']) + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains') + def test_model_derived_other_default_lookup(self): + class F(FilterSet): + class Meta: + model = Book + fields = '__all__' + + for filter_ in F.base_filters.values(): + self.assertEqual(filter_.lookup_expr, 'icontains') + def test_model_no_fields_or_exclude(self): with self.assertRaises(AssertionError) as excinfo: class F(FilterSet): @@ -401,7 +425,6 @@ class Meta: def test_meta_fields_dictionary_derived(self): class F(FilterSet): - class Meta: model = Book fields = {'price': ['exact', 'gte', 'lte'], } @@ -412,6 +435,20 @@ class Meta: expected_list = ['price', 'price__gte', 'price__lte', ] self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list)) + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='lte') + def test_meta_fields_dictionary_derived_other_default_lookup(self): + class F(FilterSet): + + class Meta: + model = Book + fields = {'price': ['exact', 'gte', 'lte'], } + + self.assertEqual(len(F.declared_filters), 0) + self.assertEqual(len(F.base_filters), 3) + + expected_list = ['price__exact', 'price__gte', 'price', ] + self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list)) + def test_meta_fields_containing_autofield(self): class F(FilterSet): username = CharFilter() @@ -634,6 +671,20 @@ class Grandchild(Child): self.assertEqual(len(Child.base_filters), 1) self.assertEqual(len(Grandchild.base_filters), 1) + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='lt') + def test_transforms_other_default_lookup(self): + class F(FilterSet): + class Meta: + model = Article + fields = { + 'published': ['lt', 'year__lt'], + } + + self.assertEqual(len(F.base_filters), 2) + + expected_list = ['published', 'published__year'] + self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list)) + def test_declared_filter_multiple_inheritance(self): class A(FilterSet): f = CharFilter() From 91526ba10ccc1883505ac5e3b0c1a922cd7fe6bd Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 4 Mar 2020 13:17:44 -0800 Subject: [PATCH 073/103] Improve docs clarity (#1181) --- docs/ref/filters.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 06319111a..1876f45a2 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -73,8 +73,8 @@ transformation and empty value checking should be unnecessary. lookup = '__'.join([name, 'isnull']) return queryset.filter(**{lookup: False}) - # alternatively, it may not be necessary to construct the lookup. - return queryset.filter(published_on__isnull=False) + # alternatively, you could opt to hardcode the lookup. e.g., + # return queryset.filter(published_on__isnull=False) class Meta: model = Book From cecd1261fa4fd9bf6a4da0f69e12d51117e3ae46 Mon Sep 17 00:00:00 2001 From: c-isaksson Date: Fri, 6 Mar 2020 20:29:48 +0100 Subject: [PATCH 074/103] Fix docs typo (#1184) --- docs/ref/filters.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 1876f45a2..d1444177b 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -578,7 +578,7 @@ Similar to a ``RangeFilter`` except it uses ISO 8601 formatted values instead of Example:: class Article(models.Model): - published = dajngo_filters.IsoDateTimeField() + published = django_filters.IsoDateTimeField() class F(FilterSet): published = IsoDateTimeFromToRangeFilter() From 4c78f08667bc8dfe4af9417c38c4e62dc61bff7f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 12 Mar 2020 05:56:22 -0700 Subject: [PATCH 075/103] Drop Django 2.1 and below (#1180) * Drop Django 2.1 and below * Update DRF test dependency * Drop Django 1.11 compat code --- django_filters/utils.py | 4 ---- tox.ini | 10 ++-------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/django_filters/utils.py b/django_filters/utils.py index e0f7bab4b..b497b3348 100644 --- a/django_filters/utils.py +++ b/django_filters/utils.py @@ -1,7 +1,6 @@ import warnings from collections import OrderedDict -import django from django.conf import settings from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models @@ -196,9 +195,6 @@ def resolve_field(model_field, lookup_expr): while lookups: name = lookups[0] args = (lhs, name) - if django.VERSION < (2, 0): - # rest_of_lookups was removed in Django 2.0 - args += (lookups,) # If there is just one part left, try first get_lookup() so # that if the lhs supports both transform and lookup for the # name, then lookup will be picked. diff --git a/tox.ini b/tox.ini index 02fc9b236..d1260d691 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,5 @@ [tox] envlist = - {py35,py36}-django111, - {py35,py36}-django20, - {py35,py36,py37}-django21, {py35,py36,py37}-django22, {py36,py37,py38}-django30, {py36,py37,py38}-latest, @@ -21,12 +18,9 @@ ignore_outcome = setenv = PYTHONDONTWRITEBYTECODE=1 deps = - django111: django==1.11.* - django20: django==2.0.* - django21: django==2.1.* django22: django==2.2.* - django30: django>=3.0,<3.1 - djangorestframework==3.10.* + django30: django>=3.0.* + djangorestframework==3.11.* latest: {[latest]deps} -rrequirements/test-ci.txt From 9feb87c321803f02c35fee18d058d0ecb3d241d5 Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Fri, 20 Mar 2020 18:24:15 +0000 Subject: [PATCH 076/103] Remove Django v1.9 support from runshell.py (#1190) --- runshell.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/runshell.py b/runshell.py index 6051442ec..48af2821a 100755 --- a/runshell.py +++ b/runshell.py @@ -1,19 +1,13 @@ #!/usr/bin/env python import os import sys -import django from django.core.management import execute_from_command_line def runshell(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") - execute_from_command_line( - sys.argv[:1] + - ['migrate', '--noinput', '-v', '0'] + - (['--run-syncdb'] if django.VERSION >= (1, 9) else [])) - - argv = sys.argv[:1] + ['shell'] + sys.argv[1:] - execute_from_command_line(argv) + execute_from_command_line(sys.argv[:1] + ['migrate', '--noinput', '-v', '0']) + execute_from_command_line(sys.argv[:1] + ['shell'] + sys.argv[1:]) if __name__ == '__main__': runshell() From f94d104b63d4b837363a354e7cdac4f6bcd5c6b1 Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Fri, 20 Mar 2020 19:08:41 +0000 Subject: [PATCH 077/103] Update supported versions (#1191) --- README.rst | 2 +- docs/guide/install.txt | 2 +- setup.py | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 685fbad18..c943818a0 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Requirements ------------ * **Python**: 3.5, 3.6, 3.7, 3.8 -* **Django**: 1.11, 2.0, 2.1, 2.2, 3.0 +* **Django**: 2.2, 3.0 * **DRF**: 3.10+ From Version 2.0 Django Filter is Python 3 only. diff --git a/docs/guide/install.txt b/docs/guide/install.txt index adadc2655..7177eebfd 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -30,5 +30,5 @@ __ http://www.django-rest-framework.org/ * **Python**: 3.5, 3.6, 3.7, 3.8 -* **Django**: 1.11, 2.0, 2.1, 2.2, 3.0 +* **Django**: 2.2, 3.0 * **DRF**: 3.10+ diff --git a/setup.py b/setup.py index 4476d6169..28b1687fa 100644 --- a/setup.py +++ b/setup.py @@ -43,9 +43,6 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Framework :: Django', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', 'Programming Language :: Python', From 93057533ca6a2ad63c45cd21017959ba1a6c0718 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Thu, 26 Mar 2020 21:55:57 +1100 Subject: [PATCH 078/103] Corrected typo in test comment. (#1194) --- tests/test_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index a46f7567e..d63074d7f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1577,6 +1577,6 @@ def test_translation_override_label(self): ]) def test_help_text(self): - # regression test for #756 - the ususal CSV help_text is not relevant to ordering filters. + # regression test for #756 - the usual CSV help_text is not relevant to ordering filters. self.assertEqual(OrderingFilter().field.help_text, '') self.assertEqual(OrderingFilter(help_text='a').field.help_text, 'a') From a5b2ace6574b35418311ea5dca7dc4014181d308 Mon Sep 17 00:00:00 2001 From: Victor Mireyev Date: Sat, 25 Apr 2020 18:15:22 +0300 Subject: [PATCH 079/103] Fix docstring typos (#1206) --- django_filters/filterset.py | 2 +- django_filters/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django_filters/filterset.py b/django_filters/filterset.py index d174718c0..f5ab4b0e4 100644 --- a/django_filters/filterset.py +++ b/django_filters/filterset.py @@ -39,7 +39,7 @@ def remote_queryset(field): """ Get the queryset for the other side of a relationship. This works - for both `RelatedField`s and `ForignObjectRel`s. + for both `RelatedField`s and `ForeignObjectRel`s. """ model = field.related_model diff --git a/django_filters/views.py b/django_filters/views.py index 81d77db1b..e9160ad93 100644 --- a/django_filters/views.py +++ b/django_filters/views.py @@ -46,7 +46,7 @@ def get_filterset(self, filterset_class): def get_filterset_kwargs(self, filterset_class): """ - Returns the keyword arguments for instanciating the filterset. + Returns the keyword arguments for instantiating the filterset. """ kwargs = { 'data': self.request.GET or None, From 188043021850757695cb37f1c3190bafce5b01b2 Mon Sep 17 00:00:00 2001 From: Christopher Malerich Date: Fri, 8 May 2020 18:12:31 -0400 Subject: [PATCH 080/103] Fix docs typo (#1195) --- docs/guide/rest_framework.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index 2384049c1..5cb8e4686 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -133,7 +133,7 @@ You can override these methods on a case-by-case basis for each view, creating u return kwargs - class BooksFilter(filters.FilterSet): + class BookFilter(filters.FilterSet): def __init__(self, *args, author=None, **kwargs): super().__init__(*args, **kwargs) # do something w/ author From eca2c1fe45fbb84c198971b7ecc2d98e690c09e1 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 14 May 2020 13:16:34 +0100 Subject: [PATCH 081/103] Python 2 cleanups (#1186) * Remove explicit inheritance from `object` * Use argumentless super() syntax Co-authored-by: Ryan P Kilby --- django_filters/conf.py | 2 +- django_filters/fields.py | 4 ++-- django_filters/filters.py | 10 +++++----- django_filters/filterset.py | 4 ++-- tests/test_conf.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/django_filters/conf.py b/django_filters/conf.py index 41a6bc533..64a68c641 100644 --- a/django_filters/conf.py +++ b/django_filters/conf.py @@ -65,7 +65,7 @@ def is_callable(value): return callable(value) and not isinstance(value, type) -class Settings(object): +class Settings: def __getattr__(self, name): if name not in DEFAULTS: diff --git a/django_filters/fields.py b/django_filters/fields.py index 930bba473..145951d70 100644 --- a/django_filters/fields.py +++ b/django_filters/fields.py @@ -211,7 +211,7 @@ def clean(self, value): return value -class ChoiceIterator(object): +class ChoiceIterator: # Emulates the behavior of ModelChoiceIterator, but instead wraps # the field's _choices iterable. @@ -257,7 +257,7 @@ def __len__(self): return super().__len__() + add -class ChoiceIteratorMixin(object): +class ChoiceIteratorMixin: def __init__(self, *args, **kwargs): self.null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL) self.null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE) diff --git a/django_filters/filters.py b/django_filters/filters.py index 0017291ce..d0e289f9c 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -62,7 +62,7 @@ ] -class Filter(object): +class Filter: creation_counter = 0 field_class = forms.Field @@ -291,7 +291,7 @@ class DurationFilter(Filter): field_class = forms.DurationField -class QuerySetRequestMixin(object): +class QuerySetRequestMixin: """ Add callable functionality to filters that support the ``queryset`` argument. If the ``queryset`` is callable, then it **must** accept the @@ -642,10 +642,10 @@ def field(self): def filter(self, qs, lookup): if not lookup: - return super(LookupChoiceFilter, self).filter(qs, None) + return super().filter(qs, None) self.lookup_expr = lookup.lookup_expr - return super(LookupChoiceFilter, self).filter(qs, lookup.value) + return super().filter(qs, lookup.value) class OrderingFilter(BaseCSVFilter, ChoiceFilter): @@ -746,7 +746,7 @@ def build_choices(self, fields, labels): return [val for pair in zip(ascending, descending) for val in pair] -class FilterMethod(object): +class FilterMethod: """ This helper is used to override Filter.filter() when a 'method' argument is passed. It proxies the call to the actual method on the filter's parent. diff --git a/django_filters/filterset.py b/django_filters/filterset.py index f5ab4b0e4..e8982671a 100644 --- a/django_filters/filterset.py +++ b/django_filters/filterset.py @@ -51,7 +51,7 @@ def remote_queryset(field): return model._default_manager.complex_filter(limit_choices_to) -class FilterSetOptions(object): +class FilterSetOptions: def __init__(self, options=None): self.model = getattr(options, 'model', None) self.fields = getattr(options, 'fields', None) @@ -184,7 +184,7 @@ def visit(name): } -class BaseFilterSet(object): +class BaseFilterSet: FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS def __init__(self, data=None, queryset=None, *, request=None, prefix=None): diff --git a/tests/test_conf.py b/tests/test_conf.py index e3f9d254c..3fb522285 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -80,7 +80,7 @@ def test_behavior(self): def func(): pass - class Class(object): + class Class: def __call__(self): pass From 9e9bb5f221c3a87ddbfb127a3e0a7e031e74e9f0 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 May 2020 16:04:53 +0200 Subject: [PATCH 082/103] Adjust tox dependencies. --- requirements/test-ci.txt | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/test-ci.txt b/requirements/test-ci.txt index d64ba8988..f10a9c33a 100644 --- a/requirements/test-ci.txt +++ b/requirements/test-ci.txt @@ -1,4 +1,4 @@ -markdown==2.6.4 +markdown coreapi django-crispy-forms diff --git a/tox.ini b/tox.ini index d1260d691..b9ed7862f 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ setenv = PYTHONDONTWRITEBYTECODE=1 deps = django22: django==2.2.* - django30: django>=3.0.* + django30: django~=3.0 djangorestframework==3.11.* latest: {[latest]deps} -rrequirements/test-ci.txt From 1ebb03a396182ea96cdc8165022ad277908413fa Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 May 2020 16:43:06 +0200 Subject: [PATCH 083/103] Fixed E741 ambiguous variable name flake8 error. --- django_filters/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_filters/filters.py b/django_filters/filters.py index d0e289f9c..8f3bd4cb7 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -623,7 +623,7 @@ def get_lookup_choices(self): field = get_model_field(self.model, self.field_name) lookups = field.get_lookups() - return [self.normalize_lookup(l) for l in lookups] + return [self.normalize_lookup(lookup) for lookup in lookups] @property def field(self): From 6fb5f3e55dadefb0c55d4040463daec763129060 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Sun, 17 May 2020 02:28:29 -0700 Subject: [PATCH 084/103] Fix pinned dependencies (#1221) --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index b9ed7862f..250837d00 100644 --- a/tox.ini +++ b/tox.ini @@ -18,9 +18,9 @@ ignore_outcome = setenv = PYTHONDONTWRITEBYTECODE=1 deps = - django22: django==2.2.* - django30: django~=3.0 - djangorestframework==3.11.* + django22: django~=2.2.0 + django30: django~=3.0.0 + djangorestframework~=3.11.0 latest: {[latest]deps} -rrequirements/test-ci.txt From 7428e47c565eb464f8abf760a68d855b2285289b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 20 May 2020 12:12:23 +0200 Subject: [PATCH 085/103] Fixed IsoDateTimeRangeFieldTests for Django 3.1 Closes #1219. ISO 8601 formats will retain tzinfo even when USE_TZ=False. --- tests/test_fields.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 9f6f8661c..2e1680924 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -97,14 +97,15 @@ def test_field(self): f = IsoDateTimeRangeField() self.assertEqual(len(f.fields), 2) - @override_settings(USE_TZ=False) def test_clean(self): w = RangeWidget() f = IsoDateTimeRangeField(widget=w) - self.assertEqual( - f.clean(['2015-01-01T10:30:01.123000+01:00', '2015-01-10T08:45:02.345000+01:00']), - slice(datetime(2015, 1, 1, 9, 30, 1, 123000), - datetime(2015, 1, 10, 7, 45, 2, 345000))) + expected = slice( + datetime(2015, 1, 1, 9, 30, 1, 123000, tzinfo=timezone.utc), + datetime(2015, 1, 10, 7, 45, 2, 345000, tzinfo=timezone.utc) + ) + actual = f.clean(['2015-01-01T10:30:01.123000+01:00', '2015-01-10T08:45:02.345000+01:00']) + self.assertEqual(expected, actual) class TimeRangeFieldTests(TestCase): From 737d75f5d5b4b0d31daed065440e637d814ffe2c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 20 May 2020 12:20:45 +0200 Subject: [PATCH 086/103] Require tests to pass on `latest`. --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 250837d00..727ed4c4f 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,6 @@ deps = [testenv] commands = coverage run --parallel-mode --source django_filters ./runtests.py --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner {posargs} -ignore_outcome = - latest: True setenv = PYTHONDONTWRITEBYTECODE=1 deps = From 56eaae15e4fab97483e1353080a0c320f18c1014 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 5 Jun 2020 16:38:40 +0200 Subject: [PATCH 087/103] Version 2.3 --- .bumpversion.cfg | 2 +- CHANGES.rst | 12 ++++++++++++ django_filters/__init__.py | 2 +- docs/conf.py | 4 ++-- setup.py | 4 ++-- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2450fcd0d..8c96352f2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.2.0 +current_version = 2.3.0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? diff --git a/CHANGES.rst b/CHANGES.rst index 7a2a41c76..0bfd96137 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,15 @@ +Version 2.3.0 (2020-6-5) +------------------------ + +* Fixed import of FieldDoesNotExist. (#1127) +* Added testing against Django 3.0. (#1125) +* Declared support for, and added testing against, Python 3.8. (#1138) +* Fix filterset multiple inheritance bug (#1131) +* Allowed customising default lookup expression. (#1129) +* Drop Django 2.1 and below (#1180) +* Fixed IsoDateTimeRangeFieldTests for Django 3.1 +* Require tests to pass against Django `master`. + Version 2.2 (2019-7-16) ----------------------- diff --git a/django_filters/__init__.py b/django_filters/__init__.py index a1eb6ffda..e42bf62b9 100644 --- a/django_filters/__init__.py +++ b/django_filters/__init__.py @@ -10,7 +10,7 @@ from . import rest_framework del pkgutil -__version__ = '2.2.0' +__version__ = '2.3.0' def parse_version(version): diff --git a/docs/conf.py b/docs/conf.py index 334e4c45c..4a9e589b6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '2.2' +version = '2.3' # The full version, including alpha/beta/rc tags. -release = '2.2.0' +release = '2.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 28b1687fa..de2d53b18 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ readme = f.read() f.close() -version = '2.2.0' +version = '2.3.0' if sys.argv[-1] == 'publish': if os.system("pip freeze | grep wheel"): @@ -56,6 +56,6 @@ zip_safe=False, python_requires='>=3.5', install_requires=[ - 'Django>=1.11', + 'Django>=2.2', ], ) From 79b4ea7535b5d6760afad736361dff08bfd1fda9 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 10 Jun 2020 16:35:25 +0000 Subject: [PATCH 088/103] Cleanup Python 2 left-overs (#1233) --- django_filters/fields.py | 10 ++-------- tests/test_filterset.py | 15 ++++----------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/django_filters/fields.py b/django_filters/fields.py index 145951d70..23f30f7a5 100644 --- a/django_filters/fields.py +++ b/django_filters/fields.py @@ -224,10 +224,7 @@ def __iter__(self): yield ("", self.field.empty_label) if self.field.null_label is not None: yield (self.field.null_value, self.field.null_label) - - # Python 2 lacks 'yield from' - for choice in self.choices: - yield choice + yield from self.choices def __len__(self): add = 1 if self.field.empty_label is not None else 0 @@ -247,10 +244,7 @@ def __iter__(self): yield next(iterable) if self.field.null_label is not None: yield (self.field.null_value, self.field.null_label) - - # Python 2 lacks 'yield from' - for value in iterable: - yield value + yield from iterable def __len__(self): add = 1 if self.field.null_label is not None else 0 diff --git a/tests/test_filterset.py b/tests/test_filterset.py index 4ccf8fd25..d3cd949cd 100644 --- a/tests/test_filterset.py +++ b/tests/test_filterset.py @@ -43,13 +43,6 @@ from .utils import MockQuerySet -def checkItemsEqual(L1, L2): - """ - TestCase.assertItemsEqual() is not available in Python 2.6. - """ - return len(L1) == len(L2) and sorted(L1) == sorted(L2) - - class HelperMethodsTests(TestCase): @unittest.skip('todo') @@ -433,7 +426,7 @@ class Meta: self.assertEqual(len(F.base_filters), 3) expected_list = ['price', 'price__gte', 'price__lte', ] - self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list)) + self.assertCountEqual(list(F.base_filters), expected_list) @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='lte') def test_meta_fields_dictionary_derived_other_default_lookup(self): @@ -447,7 +440,7 @@ class Meta: self.assertEqual(len(F.base_filters), 3) expected_list = ['price__exact', 'price__gte', 'price', ] - self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list)) + self.assertCountEqual(list(F.base_filters), expected_list) def test_meta_fields_containing_autofield(self): class F(FilterSet): @@ -476,7 +469,7 @@ class Meta: self.assertEqual(len(F.base_filters), 2) expected_list = ['id', 'username'] - self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list)) + self.assertCountEqual(list(F.base_filters), expected_list) def test_meta_fields_list_containing_unknown_fields(self): msg = ("'Meta.fields' must not contain non-model field names: " @@ -683,7 +676,7 @@ class Meta: self.assertEqual(len(F.base_filters), 2) expected_list = ['published', 'published__year'] - self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list)) + self.assertCountEqual(list(F.base_filters), expected_list) def test_declared_filter_multiple_inheritance(self): class A(FilterSet): From 434dfabdf59e6c6bba01aac7c47b07ab38e20ade Mon Sep 17 00:00:00 2001 From: Eric Theise Date: Wed, 15 Jul 2020 21:39:10 -0700 Subject: [PATCH 089/103] Update usage.txt (#1240) Fix typo, "provied" -> "provided". --- docs/guide/usage.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 7f29cf185..09c9c8cfc 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -176,7 +176,7 @@ logged-in user or the ``Accepts-Languages`` header. .. note:: - It is not guaranteed that a `request` will be provied to the `FilterSet` + It is not guaranteed that a `request` will be provided to the `FilterSet` instance. Any code depending on a request should handle the `None` case. From 906ae91cc0b471e9cf0b4e37f8d6eb8de452641d Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Mon, 20 Jul 2020 15:31:44 +0100 Subject: [PATCH 090/103] isort v5.1 compatibility (#1247) * ordered imports with isort v5.1 * --recursive flag is no long required * added --diff to output any errors to console * use extra_standard_library to add module to known_standard_library (known_standard_library now overrides standard library modules) --- django_filters/utils.py | 2 +- docs/dev/tests.txt | 2 +- setup.cfg | 2 +- tox.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/django_filters/utils.py b/django_filters/utils.py index b497b3348..125910c70 100644 --- a/django_filters/utils.py +++ b/django_filters/utils.py @@ -309,7 +309,7 @@ def translate_validation(error_dict): """ # it's necessary to lazily import the exception, as it can otherwise create # an import loop when importing django_filters inside the project settings. - from rest_framework.exceptions import ValidationError, ErrorDetail + from rest_framework.exceptions import ErrorDetail, ValidationError exc = OrderedDict( (key, [ErrorDetail(e.message % (e.params or ()), code=e.code) diff --git a/docs/dev/tests.txt b/docs/dev/tests.txt index b41469d0f..1c77634ec 100644 --- a/docs/dev/tests.txt +++ b/docs/dev/tests.txt @@ -81,7 +81,7 @@ the module imports with the appropriate `tox` env, or with `isort` directly. # or $ pip install isort - $ isort --check-only --recursive django_filters tests + $ isort --check --diff django_filters tests To sort the imports, simply remove the ``--check-only`` option. diff --git a/setup.cfg b/setup.cfg index 9f6650403..3a8d64827 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ license-file = LICENSE skip=.tox atomic=true multi_line_output=3 -known_standard_library=mock +extra_standard_library=mock known_third_party=django,pytz,rest_framework known_first_party=django_filters diff --git a/tox.ini b/tox.ini index 727ed4c4f..7f71416e6 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,7 @@ deps = -rrequirements/test-ci.txt [testenv:isort] -commands = isort --check-only --recursive django_filters tests {posargs} +commands = isort --check-only --diff django_filters tests {posargs} deps = isort [testenv:lint] From 52eece7b3b89109232669697c53e1b3001a9879b Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Tue, 21 Jul 2020 05:56:33 +0100 Subject: [PATCH 091/103] Changed `url` to `path` (#1248) * `url()` is deprecated in Django 3.1 --- tests/rest_framework/test_integration.py | 9 ++++----- tests/urls.py | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/rest_framework/test_integration.py b/tests/rest_framework/test_integration.py index 5987477d6..2c37618f8 100644 --- a/tests/rest_framework/test_integration.py +++ b/tests/rest_framework/test_integration.py @@ -1,10 +1,9 @@ import datetime from decimal import Decimal -from django.conf.urls import url from django.test import TestCase from django.test.utils import override_settings -from django.urls import reverse +from django.urls import path, reverse from django.utils.dateparse import parse_date from rest_framework import generics, serializers, status from rest_framework.test import APIRequestFactory @@ -111,9 +110,9 @@ def get_queryset(self): urlpatterns = [ - url(r'^(?P\d+)/$', FilterClassDetailView.as_view(), name='detail-view'), - url(r'^$', FilterClassRootView.as_view(), name='root-view'), - url(r'^get-queryset/$', GetQuerysetView.as_view(), name='get-queryset-view'), + path('/', FilterClassDetailView.as_view(), name='detail-view'), + path('', FilterClassRootView.as_view(), name='root-view'), + path('get-queryset/', GetQuerysetView.as_view(), name='get-queryset-view'), ] diff --git a/tests/urls.py b/tests/urls.py index 4299e1736..dc2721070 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from django_filters.views import FilterView, object_filter @@ -10,6 +10,6 @@ def _foo(): urlpatterns = [ - url(r'^books-legacy/$', object_filter, {'model': Book, 'extra_context': {'foo': _foo, 'bar': 'foo'}}), - url(r'^books/$', FilterView.as_view(model=Book)), + path('books-legacy/', object_filter, {'model': Book, 'extra_context': {'foo': _foo, 'bar': 'foo'}}), + path('books/', FilterView.as_view(model=Book)), ] From b7ed553f45b92a2d0723c51c7ea60f775d4837b2 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 2 Aug 2020 17:36:25 +0000 Subject: [PATCH 092/103] Run tests against Django 3.1 (#1252) --- README.rst | 2 +- docs/guide/install.txt | 2 +- setup.py | 1 + tox.ini | 2 ++ 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c943818a0..db3eccc32 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Requirements ------------ * **Python**: 3.5, 3.6, 3.7, 3.8 -* **Django**: 2.2, 3.0 +* **Django**: 2.2, 3.0, 3.1 * **DRF**: 3.10+ From Version 2.0 Django Filter is Python 3 only. diff --git a/docs/guide/install.txt b/docs/guide/install.txt index 7177eebfd..601765a6a 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -30,5 +30,5 @@ __ http://www.django-rest-framework.org/ * **Python**: 3.5, 3.6, 3.7, 3.8 -* **Django**: 2.2, 3.0 +* **Django**: 2.2, 3.0, 3.1 * **DRF**: 3.10+ diff --git a/setup.py b/setup.py index de2d53b18..aa8d9c5d8 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ 'Framework :: Django', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', + 'Framework :: Django :: 3.1', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', diff --git a/tox.ini b/tox.ini index 7f71416e6..576eb6cd9 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = {py35,py36,py37}-django22, {py36,py37,py38}-django30, + {py36,py37,py38}-django31, {py36,py37,py38}-latest, isort,lint,docs,warnings, @@ -18,6 +19,7 @@ setenv = deps = django22: django~=2.2.0 django30: django~=3.0.0 + django31: django>=3.1rc1,<3.2 djangorestframework~=3.11.0 latest: {[latest]deps} -rrequirements/test-ci.txt From bcea73bc5fedaa02c0202e8c498270b2954d2d6f Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Wed, 12 Aug 2020 19:06:39 +0100 Subject: [PATCH 093/103] Fixed test model deprecation (#1254) * models.NullBooleanField is deprecated in Django 3.1 --- tests/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/models.py b/tests/models.py index 9a6f1040a..2ab2af002 100644 --- a/tests/models.py +++ b/tests/models.py @@ -47,7 +47,7 @@ class User(models.Model): status = models.IntegerField(choices=STATUS_CHOICES, default=0) is_active = models.BooleanField(default=False) - is_employed = models.NullBooleanField(default=False) + is_employed = models.BooleanField(null=True, default=False) favorite_books = models.ManyToManyField('Book', related_name='lovers') From d9f389f1a408a8d4ae21f2e5a73f7a3125078905 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 25 Aug 2020 11:01:16 +0200 Subject: [PATCH 094/103] Update Jinja test dependency. --- requirements/maintainer.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/maintainer.txt b/requirements/maintainer.txt index 539fded65..cd30f9706 100644 --- a/requirements/maintainer.txt +++ b/requirements/maintainer.txt @@ -6,7 +6,7 @@ bumpversion==0.5.3 certifi==2015.9.6.2 docutils==0.12 funcsigs==0.4 -Jinja2==2.8 +Jinja2>=2.10.1 livereload==2.4.0 MarkupSafe==0.23 pathtools==0.1.2 From 85c9572b092492d3a39be35d269dbc3e7453ed69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Mon, 21 Sep 2020 18:28:02 +0200 Subject: [PATCH 095/103] Run tests with GitHub Actions --- .github/workflows/tests.yml | 78 +++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..380683cc7 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,78 @@ +--- +name: Tests +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Ensure latest setuptools + run: | + python -m pip install --upgrade pip setuptools + - name: Install dependencies + run: | + python -m pip install coverage tox tox-factor unittest-xml-reporting + - name: Run tox + run: | + python -m pip --version + python -m tox --version + python -m tox -f py$(python --version 2>&1 | cut -c 8,10) + - name: Coverage reporting + run: | + coverage combine + coverage report -m + coverage xml + coverage html + - name: Publish coverage results + uses: codecov/codecov-action@v1 + + + isort: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Ensure latest setuptools + run: | + python -m pip install --upgrade pip setuptools + - name: Install dependencies + run: | + python -m pip install tox + - name: Run tox + run: | + python -m pip --version + python -m tox --version + python -m tox -e isort,lint,docs + + + warnings: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Ensure latest setuptools + run: | + python -m pip install --upgrade pip setuptools + - name: Install dependencies + run: | + python -m pip install tox + - name: Run tox + run: | + python -m pip --version + python -m tox --version + python -m tox -e warnings diff --git a/tox.ini b/tox.ini index 576eb6cd9..e52b621ce 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ setenv = deps = django22: django~=2.2.0 django30: django~=3.0.0 - django31: django>=3.1rc1,<3.2 + django31: django~=3.1.0 djangorestframework~=3.11.0 latest: {[latest]deps} -rrequirements/test-ci.txt From 2ebce743d5facbe4fe628b79fa916a3f5ef09c21 Mon Sep 17 00:00:00 2001 From: Michael K Date: Wed, 23 Sep 2020 10:10:20 +0000 Subject: [PATCH 096/103] Confirmed compatibility with Python 3.9. (#1270) * Run tests against Python 3.9 * Don't let GitHub Actions cancel once the first job fails * [tests] Force mocked QuerySet's to be truthy Co-authored-by: Carlton Gibson --- .github/workflows/tests.yml | 3 ++- README.rst | 2 +- setup.py | 1 + tests/utils.py | 8 +++++++- tox.ini | 4 ++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 380683cc7..d4c1871e6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,8 +6,9 @@ jobs: tests: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9.0-rc - 3.9] steps: - uses: actions/checkout@v2 diff --git a/README.rst b/README.rst index db3eccc32..b8cadfac9 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ Full documentation on `read the docs`_. Requirements ------------ -* **Python**: 3.5, 3.6, 3.7, 3.8 +* **Python**: 3.5, 3.6, 3.7, 3.8, 3.9 * **Django**: 2.2, 3.0, 3.1 * **DRF**: 3.10+ diff --git a/setup.py b/setup.py index aa8d9c5d8..b9ae2141f 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Framework :: Django', ], zip_safe=False, diff --git a/tests/utils.py b/tests/utils.py index e11410eb3..3a4bd2585 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,13 +3,19 @@ from django.db import models +class QuerySet(models.QuerySet): + + def __bool__(self): + return True + + class MockQuerySet: """ Generate a mock that is suitably similar to a QuerySet """ def __new__(self): - m = mock.Mock(spec_set=models.QuerySet()) + m = mock.Mock(spec_set=QuerySet()) m.filter.return_value = m m.all.return_value = m return m diff --git a/tox.ini b/tox.ini index e52b621ce..4e9a802aa 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ envlist = {py35,py36,py37}-django22, {py36,py37,py38}-django30, - {py36,py37,py38}-django31, - {py36,py37,py38}-latest, + {py36,py37,py38,py39}-django31, + {py36,py37,py38,py39}-latest, isort,lint,docs,warnings, From 82c9a420d2ab9addcb20d6702e74afa5c04cd91f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sat, 26 Sep 2020 19:13:38 +0200 Subject: [PATCH 097/103] Added MaxValueValidator to NumberFilter. --- django_filters/filters.py | 18 ++++++++++++++++++ docs/ref/filters.txt | 6 ++++++ tests/test_forms.py | 14 ++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/django_filters/filters.py b/django_filters/filters.py index 8f3bd4cb7..8190af82e 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -2,6 +2,7 @@ from datetime import timedelta from django import forms +from django.core.validators import MaxValueValidator from django.db.models import Q from django.db.models.constants import LOOKUP_SEP from django.forms.utils import pretty_name @@ -357,6 +358,23 @@ class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter): class NumberFilter(Filter): field_class = forms.DecimalField + def get_max_validator(self): + """ + Return a MaxValueValidator for the field, or None to disable. + """ + return MaxValueValidator(1e50) + + @property + def field(self): + if not hasattr(self, '_field'): + field = super().field + max_validator = self.get_max_validator() + if max_validator: + field.validators.append(max_validator) + + self._field = field + return self._field + class NumericRangeFilter(Filter): field_class = RangeField diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index d1444177b..fcf0974dc 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -426,6 +426,12 @@ QuerySet, which then gets used as the model's manager:: Filters based on a numerical value, used with ``IntegerField``, ``FloatField``, and ``DecimalField`` by default. +.. method:: NumberFilter.get_max_validator() + + Return a ``MaxValueValidator`` instance that will be added to + ``field.validators``. By default uses a limit value of ``1e50``. Return + ``None`` to disable maximum value validation. + ``NumericRangeFilter`` ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/test_forms.py b/tests/test_forms.py index 13ca50daa..568d7482a 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -255,3 +255,17 @@ def test_is_bound_and_not_valid(self): self.assertFalse(f.is_valid()) self.assertEqual(f.data, {'price': 'four dollars'}) self.assertEqual(f.errors, {'price': ['Enter a number.']}) + + def test_number_filter_max_value_validation(self): + class F(FilterSet): + class Meta: + model = Book + fields = ['average_rating'] + + f = F({'average_rating': '1E1001'}) + self.assertTrue(f.is_bound) + self.assertFalse(f.is_valid()) + self.assertEqual( + f.errors, + {'average_rating': ['Ensure this value is less than or equal to 1e+50.']} + ) From 451d372715da9cf23d417d54a85dee16b5f59af1 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sat, 26 Sep 2020 19:44:47 +0200 Subject: [PATCH 098/103] Update docs copyright year. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4a9e589b6..c51cb2261 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,7 @@ # General information about the project. project = u'django-filter' -copyright = u'2019, Alex Gaynor, Carlton Gibson and others.' +copyright = u'2020, Alex Gaynor, Carlton Gibson and others.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From b1f56ed5a0f623f59488c95633265a33c4760505 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sat, 26 Sep 2020 19:45:22 +0200 Subject: [PATCH 099/103] Use single version reference from main module. --- docs/conf.py | 6 ++++-- setup.py | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index c51cb2261..5de3b5e91 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,8 @@ import sys, os +from django_filters import __version__ + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -48,9 +50,9 @@ # built documents. # # The short X.Y version. -version = '2.3' +version = __version__ # The full version, including alpha/beta/rc tags. -release = '2.3.0' +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index b9ae2141f..4d58eb3af 100644 --- a/setup.py +++ b/setup.py @@ -2,12 +2,12 @@ import sys from setuptools import setup, find_packages +from django_filters import __version__ + f = open('README.rst') readme = f.read() f.close() -version = '2.3.0' - if sys.argv[-1] == 'publish': if os.system("pip freeze | grep wheel"): print("wheel not installed.\nUse `pip install wheel`.\nExiting.") @@ -18,13 +18,13 @@ os.system("python setup.py sdist bdist_wheel") os.system("twine upload dist/*") print("You probably want to also tag the version now:") - print(" git tag -a %s -m 'version %s'" % (version, version)) + print(" git tag -a %s -m 'version %s'" % (__version__, __version__)) print(" git push --tags") sys.exit() setup( name='django-filter', - version=version, + version=__version__, description=('Django-filter is a reusable Django application for allowing' ' users to filter querysets dynamically.'), long_description=readme, From c045bbeb4597df2f9bb7e0c716066a13bef5a48c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sat, 26 Sep 2020 19:47:12 +0200 Subject: [PATCH 100/103] Droped using bumpversion. --- .bumpversion.cfg | 24 ------------------------ requirements/maintainer.txt | 1 - 2 files changed, 25 deletions(-) delete mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 8c96352f2..000000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,24 +0,0 @@ -[bumpversion] -current_version = 2.3.0 -commit = False -tag = False -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? -serialize = - {major}.{minor}.{patch}.{release}{num} - {major}.{minor}.{patch} - -[bumpversion:file:django_filters/__init__.py] - -[bumpversion:file:setup.py] - -[bumpversion:file:docs/conf.py] - -[bumpversion:part:release] -optional_value = final -values = - dev - final - -[bumpversion:part:num] -first_value = 1 - diff --git a/requirements/maintainer.txt b/requirements/maintainer.txt index cd30f9706..baf75e0b7 100644 --- a/requirements/maintainer.txt +++ b/requirements/maintainer.txt @@ -2,7 +2,6 @@ alabaster==0.7.7 argh==0.26.1 Babel==2.2.0 backports.ssl-match-hostname==3.4.0.2 -bumpversion==0.5.3 certifi==2015.9.6.2 docutils==0.12 funcsigs==0.4 From c9daa68e2f20a87773ad0960486a64e63feb5cf6 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sat, 26 Sep 2020 19:59:28 +0200 Subject: [PATCH 101/103] Version 20.9.0. --- CHANGES.rst | 19 +++++++++++++++++++ django_filters/__init__.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0bfd96137..cbd2b47ee 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,22 @@ +Version 20.9.0 (2020-9-27) +-------------------------- + +* SECURITY: Added a ``MaxValueValidator`` to the form field for + ``NumberFilter``. This prevents a potential DoS attack if numbers with very + large exponents were subsequently converted to integers. + + The default limit value for the validator is ``1e50``. + + The new ``NumberFilter.get_max_validator()`` allows customising the used + validator, and may return ``None`` to disable the validation entirely. + +* Added testing against Django 3.1 and Python 3.9. + + In addition tests against Django main development branch are now required to + pass. + +* Adopted `CalVer `_ versioning. + Version 2.3.0 (2020-6-5) ------------------------ diff --git a/django_filters/__init__.py b/django_filters/__init__.py index e42bf62b9..55ca971ed 100644 --- a/django_filters/__init__.py +++ b/django_filters/__init__.py @@ -10,7 +10,7 @@ from . import rest_framework del pkgutil -__version__ = '2.3.0' +__version__ = '20.9.0' def parse_version(version): From fd5824e286a8af88c2225ed647983fc163af2be0 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 27 Sep 2020 10:51:28 +0200 Subject: [PATCH 102/103] Restore version declaration in setup.py. --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4d58eb3af..2bbfff772 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,10 @@ import sys from setuptools import setup, find_packages -from django_filters import __version__ +# FIXME: Main module requires django to be present, so cannot run setup.py in +# clean environment. +# from django_filters import __version__ +__version__ = '20.9.0' f = open('README.rst') readme = f.read() From 78210722d920f803a8142e48969bc37f0f8324ed Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 27 Sep 2020 10:58:56 +0200 Subject: [PATCH 103/103] Postpone move to CalVer. --- CHANGES.rst | 4 +--- django_filters/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index cbd2b47ee..8d6e8de73 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -Version 20.9.0 (2020-9-27) +Version 2.4.0 (2020-9-27) -------------------------- * SECURITY: Added a ``MaxValueValidator`` to the form field for @@ -15,8 +15,6 @@ Version 20.9.0 (2020-9-27) In addition tests against Django main development branch are now required to pass. -* Adopted `CalVer `_ versioning. - Version 2.3.0 (2020-6-5) ------------------------ diff --git a/django_filters/__init__.py b/django_filters/__init__.py index 55ca971ed..7828c53e0 100644 --- a/django_filters/__init__.py +++ b/django_filters/__init__.py @@ -10,7 +10,7 @@ from . import rest_framework del pkgutil -__version__ = '20.9.0' +__version__ = '2.4.0' def parse_version(version): diff --git a/setup.py b/setup.py index 2bbfff772..dd129aa91 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ # FIXME: Main module requires django to be present, so cannot run setup.py in # clean environment. # from django_filters import __version__ -__version__ = '20.9.0' +__version__ = '2.4.0' f = open('README.rst') readme = f.read()