From 3a757a037cc66c71bb5aa11017f4c84bfd975b90 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 Nov 2016 20:33:19 -0500 Subject: [PATCH 01/11] Separate docs into separate sections --- docs/{img => assets}/form.png | Bin docs/{ => dev}/tests.txt | 0 docs/{ => guide}/install.txt | 0 docs/{ => guide}/migration.txt | 0 docs/{ => guide}/rest_framework.txt | 2 +- docs/{ => guide}/usage.txt | 0 docs/index.txt | 21 +++++++++++++++------ 7 files changed, 16 insertions(+), 7 deletions(-) rename docs/{img => assets}/form.png (100%) rename docs/{ => dev}/tests.txt (100%) rename docs/{ => guide}/install.txt (100%) rename docs/{ => guide}/migration.txt (100%) rename docs/{ => guide}/rest_framework.txt (99%) rename docs/{ => guide}/usage.txt (100%) diff --git a/docs/img/form.png b/docs/assets/form.png similarity index 100% rename from docs/img/form.png rename to docs/assets/form.png diff --git a/docs/tests.txt b/docs/dev/tests.txt similarity index 100% rename from docs/tests.txt rename to docs/dev/tests.txt diff --git a/docs/install.txt b/docs/guide/install.txt similarity index 100% rename from docs/install.txt rename to docs/guide/install.txt diff --git a/docs/migration.txt b/docs/guide/migration.txt similarity index 100% rename from docs/migration.txt rename to docs/guide/migration.txt diff --git a/docs/rest_framework.txt b/docs/guide/rest_framework.txt similarity index 99% rename from docs/rest_framework.txt rename to docs/guide/rest_framework.txt index 3e2b893..b3f2f4d 100644 --- a/docs/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -168,4 +168,4 @@ If you are using DRF's browsable API or admin API you may also want to install ` With crispy forms installed and added to Django's ``INSTALLED_APPS``, the browsable API will present a filtering control for ``DjangoFilterBackend``, like so: -.. image:: img/form.png +.. image:: ../assets/form.png diff --git a/docs/usage.txt b/docs/guide/usage.txt similarity index 100% rename from docs/usage.txt rename to docs/guide/usage.txt diff --git a/docs/index.txt b/docs/index.txt index 5d0c5c7..9d8f897 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -7,18 +7,27 @@ the more mundane bits of view code. Specifically, it allows users to filter down a queryset based on a model's fields, displaying the form to let them do this. -Contents: +.. toctree:: + :maxdepth: 2 + :caption: User Guide + + guide/install + guide/usage + guide/rest_framework + guide/migration .. toctree:: :maxdepth: 1 + :caption: Reference Documentation - install - usage - rest_framework ref/filterset ref/filters ref/fields ref/widgets ref/settings - migration - tests + +.. toctree:: + :maxdepth: 1 + :caption: Developer Documentation + + dev/tests From 4b459cfdab27be1547a8407f72d8db790a8f6cf7 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 Nov 2016 21:12:25 -0500 Subject: [PATCH 02/11] Rename some sections, some normalization --- docs/dev/tests.txt | 5 +++-- docs/guide/install.txt | 5 +++-- docs/guide/migration.txt | 1 + docs/guide/rest_framework.txt | 5 +++-- docs/guide/usage.txt | 5 +++-- docs/ref/fields.txt | 5 +++-- docs/ref/filters.txt | 1 + docs/ref/filterset.txt | 5 +++-- docs/ref/settings.txt | 4 +--- docs/ref/widgets.txt | 1 + 10 files changed, 22 insertions(+), 15 deletions(-) diff --git a/docs/dev/tests.txt b/docs/dev/tests.txt index fa22bf6..5da22d1 100644 --- a/docs/dev/tests.txt +++ b/docs/dev/tests.txt @@ -1,5 +1,6 @@ -Running the django-filter tests -=============================== +====================== +Running the Test Suite +====================== The easiest way to run the django-filter tests is to check out the source code into a virtualenv, where you can install the test dependencies. diff --git a/docs/guide/install.txt b/docs/guide/install.txt index 044e55e..7e940ea 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -1,5 +1,6 @@ -Installing django-filter ------------------------- +============ +Installation +============ Install with pip: diff --git a/docs/guide/migration.txt b/docs/guide/migration.txt index aa2f8ec..8448e9f 100644 --- a/docs/guide/migration.txt +++ b/docs/guide/migration.txt @@ -1,3 +1,4 @@ +================ Migrating to 1.0 ================ diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index b3f2f4d..a218f0e 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -1,5 +1,6 @@ -Django Rest Framework -===================== +==================== +Integration with DRF +==================== Integration with `Django Rest Framework`__ is provided through a DRF-specific ``FilterSet`` and a `filter backend`__. These may be found in the ``rest_framework`` sub-package. diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 180b652..09f3397 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -1,5 +1,6 @@ -Using django-filter -=================== +=============== +Getting Started +=============== Django-filter provides a simple way to filter down a queryset based on parameters a user provides. Say we have a ``Product`` model and we want to let diff --git a/docs/ref/fields.txt b/docs/ref/fields.txt index 940724f..9640917 100644 --- a/docs/ref/fields.txt +++ b/docs/ref/fields.txt @@ -1,5 +1,6 @@ -Fields Reference -================ +=============== +Field Reference +=============== ``IsoDateTimeField`` ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 0d3c1b4..69bec4d 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -1,3 +1,4 @@ +================ Filter Reference ================ diff --git a/docs/ref/filterset.txt b/docs/ref/filterset.txt index a4d851b..26b8cd9 100644 --- a/docs/ref/filterset.txt +++ b/docs/ref/filterset.txt @@ -1,5 +1,6 @@ -FilterSet Guide -=============== +================= +FilterSet Options +================= This document provides a guide on using additional FilterSet features. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 442b24e..e3f721d 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1,7 +1,5 @@ -.. _ref-settings: - ================== -Available Settings +Settings Reference ================== Here is a list of all available settings of django-filters and their diff --git a/docs/ref/widgets.txt b/docs/ref/widgets.txt index d054c58..89f4e44 100644 --- a/docs/ref/widgets.txt +++ b/docs/ref/widgets.txt @@ -1,3 +1,4 @@ +================ Widget Reference ================ From 046b31b3918439412eeed23ed88de3962e03eed3 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 8 Nov 2016 11:08:43 -0500 Subject: [PATCH 03/11] Separate tips from usage guide --- docs/guide/tips.txt | 90 ++++++++++++++++++++++++++++++++++++++++++++ docs/guide/usage.txt | 76 ------------------------------------- docs/index.txt | 1 + 3 files changed, 91 insertions(+), 76 deletions(-) create mode 100644 docs/guide/tips.txt diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt new file mode 100644 index 0000000..8548507 --- /dev/null +++ b/docs/guide/tips.txt @@ -0,0 +1,90 @@ +================== +Tips and Solutions +================== + +Common problems for declared filters +------------------------------------ + +Below are some of the common problem that occur when declaring filters. It is +recommended that you read this as it provides a more complete understanding of +how filters work. + + +Filter ``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 +filterset class will be used. Additionally, ``lookup_expr`` defaults to +``exact``. The following is an example of a misconfigured price filter: + +.. code-block:: python + + class ProductFilter(django_filters.FilterSet): + price__gt = django_filters.NumberFilter() + +The filter instance will have a field name of ``price__gt`` and an ``exact`` +lookup type. Under the hood, this will incorrectly be resolved as: + +.. code-block:: python + + Produce.objects.filter(price__gt__exact=value) + +The above will most likely generate a ``FieldError``. The correct configuration +would be: + +.. code-block:: python + + class ProductFilter(django_filters.FilterSet): + price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt') + + +Missing ``lookup_expr`` for text search filters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's quite common to forget to set the lookup expression for :code:`CharField` +and :code:`TextField` and wonder why a search for "foo" does not return results +for "foobar". This is because the default lookup type is ``exact``, but you +probably want to perform an ``icontains`` lookup. + + +Filter and lookup expression mismatch (in, range, isnull) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's not always appropriate to directly match a filter to its model field's +type, as some lookups expect different types of values. This is a commonly +found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look +at the following product model: + +.. code-block:: python + + class Product(models.Model): + category = models.ForeignKey(Category, null=True) + +Given that ``category`` is optional, it's reasonable to want to enable a search +for uncategorized products. The following is an incorrectly configured +``isnull`` filter: + +.. code-block:: python + + class ProductFilter(django_filters.FilterSet): + uncategorized = django_filters.NumberFilter(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 +only validates numbers. Filters are not `'expression aware'` and won't change +behavior based on their ``lookup_expr``. You should use filters that match the +data type of the lookup expression `instead` of the data type underlying the +model field. The following would correctly allow you to search for both +uncategorized products and products for a set of categories: + +.. code-block:: python + + class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): + pass + + class ProductFilter(django_filters.FilterSet): + categories = NumberInFilter(name='category', lookup_expr='in') + uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull') + +More info on constructing ``in`` and ``range`` csv :ref:`filters `. diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 09f3397..425cf60 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -81,82 +81,6 @@ For Django version 1.8, transformed expressions are not supported. .. _`lookup reference`: https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups -Common declarative problems -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Below are some of the common problem that occur when declaring filters. It is -recommended that you do read this as it provides a more complete understanding -on how filters work. - - -Filter ``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 -filterset class will be used. Additionally, ``lookup_expr`` defaults to -``exact``. The following is an example of a misconfigured price filter:: - - class ProductFilter(django_filters.FilterSet): - price__gt = django_filters.NumberFilter() - -The filter instance will have a field name of ``price__gt`` and an ``exact`` -lookup type. Under the hood, this will incorrectly be resolved as:: - - Produce.objects.filter(price__gt__exact=value) - -The above will most likely generate a ``FieldError``. The correct configuration -would be:: - - class ProductFilter(django_filters.FilterSet): - price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt') - - -Missing ``lookup_expr`` for text search filters -""""""""""""""""""""""""""""""""""""""""""""""" - -It's quite common to forget to set the lookup expression for :code:`CharField` -and :code:`TextField` and wonder why a search for "foo" does not return results -for "foobar". This is because the default lookup type is ``exact``, but you -probably want to perform an ``icontains`` lookup. - - -Filter and lookup expression mismatch (in, range, isnull) -""""""""""""""""""""""""""""""""""""""""""""""""""""""""" - -It's not always appropriate to directly match a filter to its model field's -type, as some lookups expect different types of values. This is a commonly -found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look -at the following product model:: - - class Product(models.Model): - category = models.ForeignKey(Category, null=True) - -Given that ``category`` is optional, it's reasonable to want to enable a search -for uncategorized products. The following is an incorrectly configured -``isnull`` filter:: - - class ProductFilter(django_filters.FilterSet): - uncategorized = django_filters.NumberFilter(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 -only validates numbers. Filters are not `'expression aware'` and won't change -behavior based on their ``lookup_expr``. You should use filters that match the -data type of the lookup expression `instead` of the data type underlying model -field. The following would correctly allow you to search for both uncategorized -products and products for a set of categories:: - - class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): - pass - - class ProductFilter(django_filters.FilterSet): - categories = NumberInFilter(name='category', lookup_expr='in') - uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull') - -More info on constructing IN and RANGE csv :ref:`filters `. - - Generating filters with Meta.fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/index.txt b/docs/index.txt index 9d8f897..cb46017 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -14,6 +14,7 @@ do this. guide/install guide/usage guide/rest_framework + guide/tips guide/migration .. toctree:: From 2b9cc1bde5476d003474b214c9eacbfad12b088d Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 8 Nov 2016 11:14:24 -0500 Subject: [PATCH 04/11] Update DRF version support --- README.rst | 6 +++--- docs/guide/install.txt | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 6de34f1..c174508 100644 --- a/README.rst +++ b/README.rst @@ -12,9 +12,9 @@ Full documentation on `read the docs`_. Requirements ------------ -* Python 2.7, 3.3, 3.4, 3.5 -* Django 1.8, 1.9, 1.10 -* DRF 3.3 (Django 1.8 only), 3.4 +* **Python**: 2.7, 3.3, 3.4, 3.5 +* **Django**: 1.8, 1.9, 1.10 +* **DRF**: 3.4, 3.5 Installation ------------ diff --git a/docs/guide/install.txt b/docs/guide/install.txt index 7e940ea..5f836f9 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -2,16 +2,32 @@ Installation ============ -Install with pip: +Django-filter can be installed from PyPI with tools like ``pip``: .. code-block:: bash - pip install django-filter + $ pip install django-filter -And then add ``'django_filters'`` to your ``INSTALLED_APPS``. +Then add ``'django_filters'`` to your ``INSTALLED_APPS``. .. note:: django-filter provides *some* localization for *some* languages. If you do not need these translations (or would rather provide your own), then it is unnecessary to add django-filter to the ``INSTALLED_APPS`` setting. + + +Requirements +------------ + +Django-filter is tested against all supported versions of Python and `Django`__, +as well as the latest versions of Django REST Framework (`DRF`__). + +__ https://www.djangoproject.com/download/ +__ http://www.django-rest-framework.org/ + + + +* **Python**: 2.7, 3.3, 3.4, 3.5 +* **Django**: 1.8, 1.9, 1.10 +* **DRF**: 3.4, 3.5 From 6e79df7ff7f802675f0f7af3473598e76c51c2a7 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 8 Nov 2016 11:16:08 -0500 Subject: [PATCH 05/11] Modernize testing docs --- docs/dev/tests.txt | 92 +++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/docs/dev/tests.txt b/docs/dev/tests.txt index 5da22d1..6410bac 100644 --- a/docs/dev/tests.txt +++ b/docs/dev/tests.txt @@ -3,67 +3,65 @@ Running the Test Suite ====================== The easiest way to run the django-filter tests is to check out the source -code into a virtualenv, where you can install the test dependencies. -django-filter uses a custom test runner to locate all of the tests, so a +code and create a virtualenv where you can install the test dependencies. +Django-filter uses a custom test runner to configure the environment, so a wrapper script is available to set up and run the test suite. .. note:: The following assumes you have `virtualenv`__ and `git`__ installed. -__ http://www.virtualenv.org -__ http://git-scm.com +__ https://virtualenv.pypa.io/en/stable/ +__ https://git-scm.com -Set up a virtualenv for the test suite --------------------------------------- +Clone the repository +-------------------- -Run the following to create a new virtualenv to run the test suite in:: +Get the source code using the following command: .. code-block:: bash - virtualenv django-filter-tests - cd django-filter-tests - . bin/activate + $ git clone https://github.com/carltongibson/django-filter.git -Get a copy of django-filter +Switch to the django-filter directory: + +.. code-block:: bash + + $ cd django-filter + +Set up the virtualenv +--------------------- + +Create a new virtualenv to run the test suite in: + +.. code-block:: bash + + $ virtualenv venv + +Then activate the virtualenv and install the test requirements: + +.. code-block:: bash + + $ source venv/bin/activate + $ pip install -r requirements/test.txt + +Execute the test runner +----------------------- + +Run the tests with the runner script: + +.. code-block:: bash + + $ python runtests.py + + +Test all supported versions --------------------------- -Get the django-filter source code using the following command:: +You can also use the excellent tox testing tool to run the tests against all +supported versions of Python and Django. Install tox, and then simply run: .. code-block:: bash - git clone https://github.com/alex/django-filter.git - -Switch to the django-filter directory:: - -.. code-block:: bash - - cd django-filter - -Install the test dependencies ------------------------------ - -Run the following to install the test dependencies within the -virutalenv:: - -.. code-block:: bash - - pip install -r requirements/test.txt - -Run the django-filter tests:: - -.. code-block:: bash - - python runtests.py - - -Testing all supported versions ------------------------------- - -You can also use the excellent tox testing tool to run the tests against all supported versions of -Python and Django. Install tox globally, and then simply run:: - -.. code-block:: bash - - tox - + $ pip install tox + $ tox From aba7021742ac70a6274f9ca38258438d0fd5f85f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 8 Nov 2016 11:23:44 -0500 Subject: [PATCH 06/11] Small fixes - thanks @bartromgens --- docs/guide/rest_framework.txt | 4 ++-- docs/guide/usage.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index a218f0e..bc4e5e8 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -73,8 +73,8 @@ To enable filtering with a ``FilterSet``, add it to the ``filter_class`` paramet filter_class = ProductFilter -Specifying ``filter_fields`` ----------------------------- +Using the ``filter_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 `. diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index 425cf60..8860a23 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -148,7 +148,7 @@ default filters for all the models fields of the same kind using models.BooleanField: { 'filter_class': django_filters.BooleanFilter, 'extra': lambda f: { - 'widget': 'forms.CheckboxInput', + 'widget': forms.CheckboxInput, }, }, } From 9fb50e5b60156c6bd42211ea93320bed06f7cdc1 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 8 Nov 2016 11:41:17 -0500 Subject: [PATCH 07/11] Remove deprecated order_by docs --- docs/ref/filterset.txt | 61 ------------------------------------------ 1 file changed, 61 deletions(-) diff --git a/docs/ref/filterset.txt b/docs/ref/filterset.txt index 26b8cd9..2f4929e 100644 --- a/docs/ref/filterset.txt +++ b/docs/ref/filterset.txt @@ -10,7 +10,6 @@ Meta options - :ref:`model ` - :ref:`fields ` - :ref:`exclude ` -- :ref:`order_by ` - :ref:`form
` - :ref:`together ` - filter_overrides @@ -92,40 +91,6 @@ declared directly on the ``FilterSet``. exclude = ['password'] -.. _order-by: - -Ordering using ``order_by`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can allow the user to control ordering by providing the -``order_by`` argument in the Filter's Meta class. ``order_by`` can be either a -``list`` or ``tuple`` of field names, in which case those are the options, or -it can be a ``bool`` which, if True, indicates that all fields that -the user can filter on can also be sorted on. An example of ordering using a list:: - - import django_filters - - class ProductFilter(django_filters.FilterSet): - price = django_filters.NumberFilter(lookup_expr='lt') - class Meta: - model = Product - fields = ['price', 'release_date'] - order_by = ['price'] - -If you want to control the display of items in ``order_by``, you can set it to -a list or tuple of 2-tuples in the format ``(field_name, display_name)``. -This lets you override the displayed names for your ordering fields:: - - order_by = ( - ('name', 'Company Name'), - ('average_rating', 'Stars'), - ) - -Note that the default query parameter name used for ordering is ``o``. You -can override this by setting an ``order_by_field`` attribute on the -``FilterSet``'s Meta class to the string value you would like to use. - - .. _form: Custom Forms using ``form`` @@ -202,29 +167,3 @@ filters for a model field, you can override ``filter_for_lookup()``. Ex:: # use default behavior otherwise return super(ProductFilter, cls).filter_for_lookup(f, lookup_type) - - -``get_ordering_field()`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want to use a custom widget, or in any other way override the ordering -field you can override the ``get_ordering_field()`` method on a ``FilterSet``. -This method just needs to return a Form Field. - -Ordering on multiple fields, or other complex orderings can be achieved by -overriding the ``FilterSet.get_order_by()`` method. This is passed the selected -``order_by`` value, and is expected to return an iterable of values to pass to -``QuerySet.order_by``. For example, to sort a ``User`` table by last name, then -first name:: - - class UserFilter(django_filters.FilterSet): - class Meta: - order_by = ( - ('username', 'Username'), - ('last_name', 'Last Name') - ) - - def get_order_by(self, order_value): - if order_value == 'last_name': - return ['last_name', 'first_name'] - return super(UserFilter, self).get_order_by(order_value) From f9c28386ab0b648b7858b4b1a7789f6f3df0d72c Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 8 Nov 2016 11:50:44 -0500 Subject: [PATCH 08/11] Small doc style fixes --- docs/ref/fields.txt | 4 +++- docs/ref/settings.txt | 2 +- docs/ref/widgets.txt | 8 ++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/ref/fields.txt b/docs/ref/fields.txt index 9640917..d73c493 100644 --- a/docs/ref/fields.txt +++ b/docs/ref/fields.txt @@ -11,7 +11,9 @@ Defines a class level attribute ``ISO_8601`` as constant for the format. Sets ``input_formats = [ISO_8601]`` — this means that by default ``IsoDateTimeField`` will **only** parse ISO 8601 formated dates. -You may set ``input_formats`` to your list of required formats as per the `DateTimeField Docs`_, using the ``ISO_8601`` class level attribute to specify the ISO 8601 format. :: +You may set ``input_formats`` to your list of required formats as per the `DateTimeField Docs`_, using the ``ISO_8601`` class level attribute to specify the ISO 8601 format. + +.. code-block:: python f = IsoDateTimeField() f.input_formats = [IsoDateTimeField.ISO_8601] + DateTimeField.input_formats diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index e3f721d..bc59ca9 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -92,6 +92,6 @@ For example, you could add verbose output for "exact" lookups. FILTERS_STRICTNESS ------------------ -DEFAULT: ``STRICTNESS.RETURN_NO_RESULTS`` +Default: ``STRICTNESS.RETURN_NO_RESULTS`` Set the global default for FilterSet :ref:`strictness `. diff --git a/docs/ref/widgets.txt b/docs/ref/widgets.txt index 89f4e44..6af96d5 100644 --- a/docs/ref/widgets.txt +++ b/docs/ref/widgets.txt @@ -30,7 +30,9 @@ placeholders: 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. -To use it, pass this into the ``widgets`` argument of the ``BooleanFilter``:: +To use it, pass this into the ``widgets`` argument of the ``BooleanFilter``: + +.. code-block:: python active = BooleanFilter(widget=BooleanWidget()) @@ -54,6 +56,8 @@ 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 the same arguments and values. To use it, pass it to ``widget`` argument of -a ``RangeField``:: +a ``RangeField``: + +.. code-block:: python date_range = DateFromToRangeFilter(widget=RangeWidget(attrs={'placeholder': 'YYYY/MM/DD'})) From 2cdbf6f2c2301e911d2b75fef371077308ce479f Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 8 Nov 2016 13:42:02 -0500 Subject: [PATCH 09/11] Add empty/null value filtering docs --- docs/guide/tips.txt | 118 +++++++++++++++++++++++++++++++++++++++++++ docs/ref/filters.txt | 2 + 2 files changed, 120 insertions(+) diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index 8548507..a9232bd 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -88,3 +88,121 @@ uncategorized products and products for a set of categories: uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull') More info on constructing ``in`` and ``range`` csv :ref:`filters `. + + +Filtering by empty values +------------------------- + +There are a number of cases where you may need to filter by empty or null +values. The following are some common solutions to these problems: + + +Filtering by null values +~~~~~~~~~~~~~~~~~~~~~~~~ + +As explained in the above "Filter and lookup expression mismatch" section, a +common problem is how to correctly filter by null values on a field. + +Solution 1: Using a ``BooleanFilter`` with ``isnull`` +""""""""""""""""""""""""""""""""""""""""""""""""""""" + +Using ``BooleanFilter`` with an ``isnull`` lookup is a builtin solution used by +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') + +.. note:: + + Remember that the filter class is validating the input value. The underlying + type of the mode field is not relevant here. + +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) + +Solution 2: Using ``ChoiceFilter``'s null choice +"""""""""""""""""""""""""""""""""""""""""""""""" + +If you're using a ChoiceFilter, you may also filter by null values by enabling +the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference +:ref:`docs `. + +.. code-block:: python + + class ProductFilter(django_filters.FilterSet): + category = django_filters.ModelChoiceFilter( + name='category', lookup_expr='isnull', + null_label='Uncategorized', + queryset=Category.objects.all(), + ) + + +Filtering by an empty string +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's not currently possible to filter by an empty string, since empty values are +interpreted as a skipped filter. + +.. code-block:: http + + GET http://localhost/api/my-model?myfield= + +Solution 1: magic values +"""""""""""""""""""""""" + +You can override the ``filter()`` method of a filter class to specifically check +for magic values. This is similar to the ``ChoiceFilter``'s null value handling. + +.. code-block:: http + + GET http://localhost/api/my-model?myfield=EMPTY + +.. code-block:: python + + class MyCharFilter(filters.CharFilter): + empty_value = 'EMPTY' + + def filter(self, qs, value): + if value != self.empty_value: + return super(MyCharFilter, self).filter(qs, value) + + qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): ""}) + return qs.distinct() if self.distinct else qs + + +Solution 2: empty string filter +""""""""""""""""""""""""""""""" + +It would also be possible to create an empty value filter that exhibits the same +behavior as an ``isnull`` filter. + +.. code-block:: http + + GET http://localhost/api/my-model?myfield__isempty=false + +.. code-block:: python + + from django.core.validators import EMPTY_VALUES + + class EmptyStringFilter(filters.BooleanFilter): + def filter(self, qs, value): + if value in EMPTY_VALUES: + return qs + + exclude = self.exclude ^ (value is False) + method = qs.exclude if exclude else qs.filter + + return method(**{self.name: ""}) + + + class MyFilterSet(filters.FilterSet): + myfield__isempty = EmptyStringFilter(name='myfield') + + class Meta: + model = MyModel diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index 69bec4d..556f0b0 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -192,6 +192,8 @@ This filter matches UUID values, used with ``models.UUIDField`` by default. This filter matches a boolean, either ``True`` or ``False``, used with ``BooleanField`` and ``NullBooleanField`` by default. +.. _choice-filter: + ``ChoiceFilter`` ~~~~~~~~~~~~~~~~ From 19afd2cf2ad760401a14c0dd7bb7dba7a7ebb062 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 8 Nov 2016 14:10:48 -0500 Subject: [PATCH 10/11] Document multi-field approach to handle null value --- docs/guide/tips.txt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index a9232bd..2526493 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -142,6 +142,13 @@ the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference queryset=Category.objects.all(), ) +Solution 3: Comining fields w/ ``MultiValueField`` +"""""""""""""""""""""""""""""""""""""""""""""""""" + +An alternative approach is to use Django's ``MultiValueField`` to manually add +in a ``BooleanField`` to handle null values. Proof of concept: +https://github.com/carltongibson/django-filter/issues/446 + Filtering by an empty string ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -153,7 +160,7 @@ interpreted as a skipped filter. GET http://localhost/api/my-model?myfield= -Solution 1: magic values +Solution 1: Magic values """""""""""""""""""""""" You can override the ``filter()`` method of a filter class to specifically check @@ -176,7 +183,7 @@ for magic values. This is similar to the ``ChoiceFilter``'s null value handling. return qs.distinct() if self.distinct else qs -Solution 2: empty string filter +Solution 2: Empty string filter """"""""""""""""""""""""""""""" It would also be possible to create an empty value filter that exhibits the same From 2159d9063bd5b1065401bd411f78bf2a529aaf6a Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 8 Nov 2016 20:31:34 +0100 Subject: [PATCH 11/11] Fix typo --- docs/guide/tips.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index 2526493..7b46a8d 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -142,7 +142,7 @@ the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference queryset=Category.objects.all(), ) -Solution 3: Comining fields w/ ``MultiValueField`` +Solution 3: Combining fields w/ ``MultiValueField`` """""""""""""""""""""""""""""""""""""""""""""""""" An alternative approach is to use Django's ``MultiValueField`` to manually add