Merge pull request #554 from rpkilby/docs-changes

Documentation updates
This commit is contained in:
Carlton Gibson 2016-11-08 20:37:39 +01:00 committed by GitHub
commit 677b48f145
16 changed files with 366 additions and 249 deletions

View File

@ -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
------------

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

67
docs/dev/tests.txt Normal file
View File

@ -0,0 +1,67 @@
======================
Running the Test Suite
======================
The easiest way to run the django-filter tests is to check out the source
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.
__ https://virtualenv.pypa.io/en/stable/
__ https://git-scm.com
Clone the repository
--------------------
Get the source code using the following command:
.. code-block:: bash
$ git clone https://github.com/carltongibson/django-filter.git
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
---------------------------
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
$ pip install tox
$ tox

33
docs/guide/install.txt Normal file
View File

@ -0,0 +1,33 @@
============
Installation
============
Django-filter can be installed from PyPI with tools like ``pip``:
.. code-block:: bash
$ pip install django-filter
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

View File

@ -1,3 +1,4 @@
================
Migrating to 1.0
================

View File

@ -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.
@ -72,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 <fields>`.
@ -168,4 +169,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

215
docs/guide/tips.txt Normal file
View File

@ -0,0 +1,215 @@
==================
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 <base-in-filter>`.
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 <choice-filter>`.
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
category = django_filters.ModelChoiceFilter(
name='category', lookup_expr='isnull',
null_label='Uncategorized',
queryset=Category.objects.all(),
)
Solution 3: Combining 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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

View File

@ -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
@ -80,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 <base-in-filter>`.
Generating filters with Meta.fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -223,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,
},
},
}

View File

@ -7,18 +7,28 @@ 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/tips
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

View File

@ -1,16 +0,0 @@
Installing django-filter
------------------------
Install with pip:
.. code-block:: bash
pip install django-filter
And 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.

View File

@ -1,5 +1,6 @@
Fields Reference
================
===============
Field Reference
===============
``IsoDateTimeField``
~~~~~~~~~~~~~~~~~~~~
@ -10,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

View File

@ -1,3 +1,4 @@
================
Filter Reference
================
@ -191,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``
~~~~~~~~~~~~~~~~

View File

@ -1,5 +1,6 @@
FilterSet Guide
===============
=================
FilterSet Options
=================
This document provides a guide on using additional FilterSet features.
@ -9,7 +10,6 @@ Meta options
- :ref:`model <model>`
- :ref:`fields <fields>`
- :ref:`exclude <exclude>`
- :ref:`order_by <order-by>`
- :ref:`form <form>`
- :ref:`together <together>`
- filter_overrides
@ -91,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``
@ -201,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)

View File

@ -1,7 +1,5 @@
.. _ref-settings:
==================
Available Settings
Settings Reference
==================
Here is a list of all available settings of django-filters and their
@ -94,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 <strict>`.

View File

@ -1,3 +1,4 @@
================
Widget Reference
================
@ -29,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())
@ -53,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'}))

View File

@ -1,68 +0,0 @@
Running the django-filter tests
===============================
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
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
Set up a virtualenv for the test suite
--------------------------------------
Run the following to create a new virtualenv to run the test suite in::
.. code-block:: bash
virtualenv django-filter-tests
cd django-filter-tests
. bin/activate
Get a copy of django-filter
---------------------------
Get the django-filter source code using the following command::
.. 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