Giter Site home page Giter Site logo

django-rest-framework-filters's Introduction

Django Rest Framework Filters

image

image

image

image

image

django-rest-framework-filters is an extension to Django REST framework and Django filter that makes it easy to filter across relationships. Historically, this extension also provided a number of additional features and fixes, however the number of features has shrunk as they are merged back into django-filter.

Using django-rest-framework-filters, we can easily do stuff like:

/api/article?author__first_name__icontains=john
/api/article?is_published!=true

! These docs pertain to the upcoming 1.0 release. Current docs can be found here.


! The 1.0 pre-release is compatible with django-filter 2.x and can be installed with pip install --pre.


Table of Contents

Features

  • Easy filtering across relationships.
  • Support for method filtering across relationships.
  • Automatic filter negation with a simple param!=value syntax.
  • Backend for complex operations on multiple filtered querysets. eg, q1 | q2.

Requirements

  • Python: 3.5, 3.6, 3.7, 3.8
  • Django: 1.11, 2.0, 2.1, 2.2, 3.0, 3.1
  • DRF: 3.11
  • django-filter: 2.1, 2.2 (Django 2.0+)

Installation

Install with pip, or your preferred package manager:

$ pip install djangorestframework-filters

Add to your INSTALLED_APPS setting:

INSTALLED_APPS = [
    'rest_framework_filters',
    ...
]

FilterSet usage

Upgrading from django-filter to django-rest-framework-filters is straightforward:

  • Import from rest_framework_filters instead of from django_filters
  • Use the rest_framework_filters backend instead of the one provided by django_filter.
# django-filter
from django_filters.rest_framework import FilterSet, filters

class ProductFilter(FilterSet):
    manufacturer = filters.ModelChoiceFilter(queryset=Manufacturer.objects.all())
    ...


# django-rest-framework-filters
import rest_framework_filters as filters

class ProductFilter(filters.FilterSet):
    manufacturer = filters.ModelChoiceFilter(queryset=Manufacturer.objects.all())
    ...

To use the django-rest-framework-filters backend, add the following to your settings:

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': (
        'rest_framework_filters.backends.RestFrameworkFilterBackend', ...
    ),
    ...

Once configured, you can continue to use all of the filters found in django-filter.

Filtering across relationships

You can easily traverse multiple relationships when filtering by using RelatedFilter:

from rest_framework import viewsets
import rest_framework_filters as filters


class ManagerFilter(filters.FilterSet):
    class Meta:
        model = Manager
        fields = {'name': ['exact', 'in', 'startswith']}


class DepartmentFilter(filters.FilterSet):
    manager = filters.RelatedFilter(ManagerFilter, field_name='manager', queryset=Manager.objects.all())

    class Meta:
        model = Department
        fields = {'name': ['exact', 'in', 'startswith']}


class CompanyFilter(filters.FilterSet):
    department = filters.RelatedFilter(DepartmentFilter, field_name='department', queryset=Department.objects.all())

    class Meta:
        model = Company
        fields = {'name': ['exact', 'in', 'startswith']}


# company viewset
class CompanyView(viewsets.ModelViewSet):
    filter_class = CompanyFilter
    ...

Example filter calls:

/api/companies?department__name=Accounting
/api/companies?department__manager__name__startswith=Bob

queryset callables

Since RelatedFilter is a subclass of ModelChoiceFilter, the queryset argument supports callable behavior. In the following example, the set of departments is restricted to those in the user's company.

def departments(request):
    company = request.user.company
    return company.department_set.all()

class EmployeeFilter(filters.FilterSet):
    department = filters.RelatedFilter(filterset=DepartmentFilter, queryset=departments)
    ...

Recursive & Circular relationships

Recursive relations are also supported. Provide the module path as a string in place of the filterset class.

class PersonFilter(filters.FilterSet):
    name = filters.AllLookupsFilter(field_name='name')
    best_friend = filters.RelatedFilter('people.views.PersonFilter', field_name='best_friend', queryset=Person.objects.all())

    class Meta:
        model = Person

This feature is also useful for circular relationships, where a related filterset may not yet be created. Note that you can pass the related filterset by name if it's located in the same module as the parent filterset.

class BlogFilter(filters.FilterSet):
    post = filters.RelatedFilter('PostFilter', queryset=Post.objects.all())

class PostFilter(filters.FilterSet):
    blog = filters.RelatedFilter('BlogFilter', queryset=Blog.objects.all())

Supporting Filter.method

django_filters.MethodFilter has been deprecated and reimplemented as the method argument to all filter classes. It incorporates some of the implementation details of the old rest_framework_filters.MethodFilter, but requires less boilerplate and is simpler to write.

  • It is no longer necessary to perform empty/null value checking.
  • You may use any filter class (CharFilter, BooleanFilter, etc...) which will validate input values for you.
  • The argument signature has changed from (name, qs, value) to (qs, name, value).
class PostFilter(filters.FilterSet):
    # Note the use of BooleanFilter, the original model field's name, and the method argument.
    is_published = filters.BooleanFilter(field_name='date_published', method='filter_is_published')

    class Meta:
        model = Post
        fields = ['title', 'content']

    def filter_is_published(self, qs, name, value):
        """
        `is_published` is based on the `date_published` model field.
        If the publishing date is null, then the post is not published.
        """
        # incoming value is normalized as a boolean by BooleanFilter
        isnull = not value
        lookup_expr = LOOKUP_SEP.join([name, 'isnull'])

        return qs.filter(**{lookup_expr: isnull})

class AuthorFilter(filters.FilterSet):
    posts = filters.RelatedFilter('PostFilter', queryset=Post.objects.all())

    class Meta:
        model = Author
        fields = ['name']

The above would enable the following filter calls:

/api/posts?is_published=true
/api/authors?posts__is_published=true

In the first API call, the filter method receives a queryset of posts. In the second, it receives a queryset of users. The filter method in the example modifies the lookup name to work across the relationship, allowing you to find published posts, or authors who have published posts.

Automatic Filter Negation/Exclusion

FilterSets support automatic exclusion using a simple param!=value syntax. This syntax internally sets the exclude property on the filter.

/api/page?title!=The%20Park

This syntax supports regular filtering combined with exclusion filtering. For example, the following would search for all articles containing "Hello" in the title, while excluding those containing "World".

/api/articles?title__contains=Hello&title__contains!=World

Note that most filters only accept a single query parameter. In the above, title__contains and title__contains! are interpreted as two separate query parameters. The following would probably be invalid, although it depends on the specifics of the individual filter class:

/api/articles?title__contains=Hello&title__contains!=World&title_contains!=Friend

Allowing any lookup type on a field

If you need to enable several lookups for a field, django-filter provides the dict-syntax for Meta.fields.

class ProductFilter(filters.FilterSet):
    class Meta:
        model = Product
        fields = {
            'price': ['exact', 'lt', 'gt', ...],
        }

django-rest-framework-filters also allows you to enable all possible lookups for any field. This can be achieved through the use of AllLookupsFilter or using the '__all__' value in the Meta.fields dict-style syntax. Generated filters (Meta.fields, AllLookupsFilter) will never override your declared filters.

Note that using all lookups comes with the same admonitions as enabling '__all__' fields in django forms (docs). Exposing all lookups may allow users to construct queries that inadvertently leak data. Use this feature responsibly.

class ProductFilter(filters.FilterSet):
    # Not overridden by `__all__`
    price__gt = filters.NumberFilter(field_name='price', lookup_expr='gt', label='Minimum price')

    class Meta:
        model = Product
        fields = {
            'price': '__all__',
        }

# or

class ProductFilter(filters.FilterSet):
    price = filters.AllLookupsFilter()

    # Not overridden by `AllLookupsFilter`
    price__gt = filters.NumberFilter(field_name='price', lookup_expr='gt', label='Minimum price')

    class Meta:
        model = Product

You cannot combine AllLookupsFilter with RelatedFilter as the filter names would clash.

class ProductFilter(filters.FilterSet):
    manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all())
    manufacturer = filters.AllLookupsFilter()

To work around this, you have the following options:

class ProductFilter(filters.FilterSet):
    manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all())

    class Meta:
        model = Product
        fields = {
            'manufacturer': '__all__',
        }

# or

class ProductFilter(filters.FilterSet):
    manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all(), lookups='__all__')  # `lookups` also accepts a list

    class Meta:
        model = Product

Can I mix and match django-filter and django-rest-framework-filters?

Yes you can. django-rest-framework-filters is simply an extension of django-filter. Note that RelatedFilter and other django-rest-framework-filters features are designed to work with rest_framework_filters.FilterSet and will not function on a django_filters.FilterSet. However, the target RelatedFilter.filterset may point to a FilterSet from either package, and both FilterSet implementations are compatible with the other's DRF backend.

# valid
class VanillaFilter(django_filters.FilterSet):
    ...

class DRFFilter(rest_framework_filters.FilterSet):
    vanilla = rest_framework_filters.RelatedFilter(filterset=VanillaFilter, queryset=...)


# invalid
class DRFFilter(rest_framework_filters.FilterSet):
    ...

class VanillaFilter(django_filters.FilterSet):
    drf = rest_framework_filters.RelatedFilter(filterset=DRFFilter, queryset=...)

Caveats & Limitations

MultiWidget is incompatible

djangorestframework-filters is not compatible with form widgets that parse query names that differ from the filter's attribute name. Although this only practically applies to MultiWidget, it is a general limitation that affects custom widgets that also have this behavior. Affected filters include RangeFilter, DateTimeFromToRangeFilter, DateFromToRangeFilter, TimeRangeFilter, and NumericRangeFilter.

To demonstrate the incompatiblity, take the following filterset:

class PostFilter(FilterSet):
    publish_date = filters.DateFromToRangeFilter()

The above filter allows users to perform a range query on the publication date. The filter class internally uses MultiWidget to separately parse the upper and lower bound values. The incompatibility lies in that MultiWidget appends an index to its inner widget names. Instead of parsing publish_date, it expects publish_date_0 and publish_date_1. It is possible to fix this by including the attribute name in the querystring, although this is not recommended.

?publish_date_0=2016-01-01&publish_date_1=2016-02-01&publish_date=

MultiWidget is also discouraged since:

  • core-api field introspection fails for similar reasons
  • _0 and _1 are less API-friendly than _min and _max

The recommended solutions are to either:

  • Create separate filters for each of the sub-widgets (such as publish_date_min and publish_date_max).
  • Use a CSV-based filter such as those derived from BaseCSVFilter/BaseInFilter/BaseRangeFilter. eg,
?publish_date__range=2016-01-01,2016-02-01

Complex Operations

The ComplexFilterBackend defines a custom querystring syntax and encoding process that enables the expression of complex queries. This syntax extends standard querystrings with the ability to define multiple sets of parameters and operators for how the queries should be combined.


! Note that this feature is experimental. Bugs may be encountered, and the backend is subject to change.


To understand the backend more fully, consider a query to find all articles that contain titles starting with either "Who" or "What". The underlying query could be represented with the following:

q1 = Article.objects.filter(title__startswith='Who')
q2 = Article.objects.filter(title__startswith='What')
return q1 | q2

Now consider the query, but modified with upper and lower date bounds:

q1 = Article.objects.filter(title__startswith='Who').filter(publish_date__lte='2005-01-01')
q2 = Article.objects.filter(title__startswith='What').filter(publish_date__gte='2010-01-01')
return q1 | q2

Using just a FilterSet, it is certainly feasible to represent the former query by writing a custom filter class. However, it is less feasible with the latter query, where multiple sets of varying data types and lookups need to be validated. In contrast, the ComplexFilterBackend can create this complex query through the arbitrary combination of a simple filter. To support the above, the querystring needs to be created with minimal changes. Unencoded example:

(title__startswith=Who&publish_date__lte=2005-01-01) | (title__startswith=What&publish_date__gte=2010-01-01)

By default, the backend combines queries with both & (AND) and | (OR), and supports unary negation ~. E.g.,

(param1=value1) & (param2=value2) | ~(param3=value3)

The backend supports both standard and complex queries. To perform complex queries, the query must be encoded and set as the value of the complex_filter_param (defaults to filters). To perform standard queries, use the backend in the same manner as the RestFrameworkFilterBackend.

Configuring ComplexFilterBackend

Similar to other backends, ComplexFilterBackend must be added to a view's filter_backends atribute. Either add it to the DEFAULT_FILTER_BACKENDS setting, or set it as a backend on the view class.

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': (
        'rest_framework_filters.backends.ComplexFilterBackend',
    ),
}

# or

class MyViewSet(generics.ListAPIView):
    filter_backends = (rest_framework_filters.backends.ComplexFilterBackend, )
    ...

You may customize how queries are combined by subclassing ComplexFilterBackend and overriding the operators attribute. operators is a map of operator symbols to functions that combine two querysets. For example, the map can be overridden to use the QuerySet.intersection() and QuerySet.union() instead of & and |.

class CustomizedBackend(ComplexFilterBackend):
    operators = {
        '&': QuerySet.intersection,
        '|': QuerySet.union,
        '-': QuerySet.difference,
    }

Unary negation relies on ORM internals and may be buggy in certain circumstances. If there are issues with this feature, it can be disabled by setting the negation attribute to False on the backend class. If you do experience bugs, please open an issue on the bug tracker.

Complex querystring encoding

Below is the procedure for encoding a complex query:

  • Convert the query paramaters into individual querystrings.
  • URL-encode the individual querystrings.
  • Wrap the encoded strings in parentheses, and join with operators.
  • URL-encode the entire querystring.
  • Set as the value to the complex filter param (e.g., ?filters=<complex querystring>).

Note that filters is the default parameter name and can be overridden in the backend class.

Using the first example, these steps can be visualized as so:

  • title__startswith=Who, title__startswith=What
  • title__startswith%3DWho, title__startswith%3DWhat
  • (title__startswith%3DWho) | (title__startswith%3DWhat)
  • %28title__startswith%253DWho%29%20%7C%20%28title__startswith%253DWhat%29
  • filters=%28title__startswith%253DWho%29%20%7C%20%28title__startswith%253DWhat%29

Error handling

ComplexFilterBackend will raise any decoding errors under the complex filtering parameter name. For example,

{
    "filters": [
        "Invalid querystring operator. Matched: 'foo'."
    ]
}

When filtering the querysets, filterset validation errors will be collected and raised under the complex filtering parameter name, then under the filterset's decoded querystring. For a complex query like (a=1&b=2) | (c=3&d=4), errors would be raised like so:

{
    "filters": {
        "a=1&b=2": {
            "a": ["..."]
        },
        "c=3&d=4": {
            "c": ["..."]
        }
    }
{

Migrating to 1.0

Backend renamed, provides new templates

The backend has been renamed from DjangoFilterBackend to RestFrameworkFilterBackend and now uses its own template paths, located under rest_framework_filters instead of django_filters/rest_framework.

To load the included templates, it is necessary to add rest_framework_filters to the INSTALLED_APPS setting.

RelatedFilter.queryset now required

The related filterset's model is no longer used to provide the default value for RelatedFilter.queryset. This change reduces the chance of unintentionally exposing data in the rendered filter forms. You must now explicitly provide the queryset argument, or override the get_queryset() method (see queryset callables).

get_filters() renamed to get_request_filters()

django-filter has add a get_filters() classmethod to it's API, so this method has been renamed.

Publishing

$ pip install -U twine setuptools wheel
$ rm -rf dist/ build/
$ python setup.py sdist bdist_wheel
$ twine upload dist/*

Copyright (c) 2013-2015 Philip Neustrom & 2016-2019 Ryan P Kilby. See LICENSE for details.

django-rest-framework-filters's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-rest-framework-filters's Issues

Problem with Django 1.9

Hi

I have seen that your Django version is 1.8+ though with Django 1.9.7 I get the following error message:
rest_framework_filters/filterset.py", line 12, in
from django.utils.datastructures import SortedDict
ImportError: cannot import name 'SortedDict'

Can you please fix that?

Combining filters in query parameters (e.g. color=green&color=blue)

Wondering why django-rest-framework-filters does not support using multiple values for the same field like color=green&color=blue. In that case, I'm only getting blue results instead of blue + green.

This functionality is supported in django_filters...

Is it possible to make my filters support this?

Circular RelatedFilter configuration? Impossible?

I have a database of judges each of whom have degrees from specific schools.

I want to use my API to either query judges that went to a specific school:

/api/judges/?educations__school_name=vanderbilt

Or I want to filter which judges went to a specific school:

/api/schools/?judges__race=black

I can set up a RelatedFilter on one of these, but when I set it up on the second one, it can't find the class.

This problem is handled in Django models by allowing strings for ForeignKey names:

 appointer = models.ForeignKey(
    'Politician',
)

Is there a way to do this with Django filters?

Thanks for the great package.

Use filters_for_model to reimplement '__all__'

Given the following filterset,

class F(FilterSet):
    email = filters.CharFilter()

    class Meta:
        model = User
        fields = {'email: '__all__'}

There are a few problems:

  • an AllLookupsFilter will be created that overwrites the existing CharFilter for email. Declared filters should have precedence over Meta.fileds.
  • modifying Meta.fields was "The Wrong Thing To Do" apparently, even though it 'works'. If a user inspects the fields dict, it is confusing when the key is no longer present. We need to hook into filters_for_model for a proper solution. There was also #81, but that was solved by copying the dict.

Todo:

  • Hook into filters_for_model to reimplement __all__ handling.
  • Remove FilterSet.convert__all__() and FilterBackend.get_filter_class(). Deprecations aren't necessary as convert__all__ is an internal method and get_filter_class does not modify the default behavior.

FilterSet.fix_filter_field not called for ALL_LOOKUPS dictionary style declarations

The docs imply that ALL_LOOKUPS dictionary style declarations are equivalent to use of filters.AllLookupsFilter:

 ...we sometimes want to allow any possible lookups on particular fields. We do this by using AllLookupsFilter:
<snip>
Additionally, you may use ALL_LOOKUPS with dictionary style declarations.

However, these two approaches are not equivalent because the dictionary approach with ALL_LOOKUPS does not call FilterSet.fix_filter_field method to make filter field / lookup adjustments. That method is only called when the filter is as actually an instance of filters.AllLookupsFilter.

class FilterSetMetaclass(filterset.FilterSetMetaclass):
    def __new__(cls, name, bases, attrs):
        new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)

        # Populate our FilterSet fields with all the possible
        # filters for the AllLookupsFilter field.
        for name, filter_ in six.iteritems(new_class.base_filters.copy()):
            if isinstance(filter_, filters.AllLookupsFilter):
                model = new_class._meta.model
                field = filterset.get_model_field(model, filter_.name)

                for lookup_type in django_filters.filters.LOOKUP_TYPES:
                    if isinstance(field, ForeignObjectRel):
                        f = new_class.filter_for_reverse_field(field, filter_.name)
                    else:
                        f = new_class.filter_for_field(field, filter_.name)
                    f.lookup_type = lookup_type
                    f = new_class.fix_filter_field(f)

v0.8.0 release

Hi @philipn - could you review and release v0.8.0 to pypi? This brings drf-filters up-to-date with the recent django-filter 0.13.0 release

Changelog:

v0.8.0
------

This release is tied to a major update of django-filter (more details in #66),
which fixes how lookup expressions are resolved. 'in', 'range', and 'isnull'
lookups no longer require special handling by django-rest-framework-filters.
This has the following effects:

  * Deprecates ArrayDecimalField/InSetNumberFilter
  * Deprecates ArrayCharField/InSetCharFilter
  * Deprecates FilterSet.fix_filter_field
  * Deprecates ALL_LOOKUPS in favor of '__all__' constant.
  * AllLookupsFilter now generates only valid lookup expressions.

* #2 'range' lookup types do not work
* #15 Date lookup types do not work (year, day, ...)
* #16 'in' lookup types do not work
* #64 Fix browsable API filter form
* #69 Fix compatibility with base django-filter `FilterSet`s
* #70 Refactor related filter handling, fixing some edge cases
* Deprecated 'cache' argument to FilterSet
* #73 Warn use of `order_by`

Errors when using rest_framework_filters.backends.DjangoFilterBackend

I sometimes get errors stemming from the return value in the filter_queryset() method of rest_framework_filters.backends.DjangoFilterBackend. If I switch to using rest_framework.filters.DjangoFilterBackend things work fine. I'm having a hard time quantifying it beyond that but here are two snippets of tracebacks that might give some context.

This is the more common one:

File "/home/vagrant/venv/lib/python2.7/site-packages/rest_framework_filters/backends.py" in filter_queryset
  48.             return _filter.qs
File "/home/vagrant/venv/lib/python2.7/site-packages/django_filters/filterset.py" in qs
  314.                     value = self.form.cleaned_data[name]

Exception Type: AttributeError at /esdb_api/v1/contact/
Exception Value: 'ContactFilterForm' object has no attribute 'cleaned_data'

I also see this traceback, but note that I am not using any filters in the request that generated this traceback:

File "/home/vagrant/venv/lib/python2.7/site-packages/rest_framework_filters/backends.py" in filter_queryset
  48.             return _filter.qs
File "/home/vagrant/venv/lib/python2.7/site-packages/django_filters/filterset.py" in qs
  314.                     value = self.form.cleaned_data[name]

Exception Type: KeyError at /esdb_api/v1/contact/
Exception Value: u'location__consoles__lt'

No field filters form on the browsable API

I have a rest_framework_filters.FilterSet-derived class which works perfectly for filtering but it doesn't show any field filters in the browsable API.

I've been scouring the web for information about this but just can't find anything. Is it a known limitation that I somehow missed, or is it a bug? Maybe related to #40 (Use new DRF-friendly FilterSet & Backend)?

Unexpected keyword argument cache

After upgrading past 0.4.0, I get an error when filtering on a queryset...

got an unexpected keyword arguments cache

Is there some configuration I am missing? I am using a generic view set and on django rest 3.3.2.

return filter_class(request.query_params, queryset=queryset, cache=cache).qs

https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/filters.py#L85

Upgrade to 0.8.0 'unknown output field'

Cannot resolve unknown expression type, unknown output field

Any thoughts on what I am missing in my app? I see a utils.py file was added, but having trouble tracing down the problem.
I'm on the latest Django & django filter, and rest framework packages. Any thoughts on what I am missing b/w the jump from 0.7 to 0.8?

class PersonFilter(FilterSet):

    full_name = AllLookupsFilter(name='full_name')

    class Meta:
        model = Person


class PersonViewSet(viewsets.ModelViewSet):

    resource_name = 'people'
    model = Person
    queryset = Person.objects.all()
    filter_class = PersonFilter
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = ps.PersonListSerializer 
    filter_backends = (backends.DjangoFilterBackend, filters.SearchFilter,)
    search_fields = ('username', 'first_name', 'last_name', 'title', 'status__name')

AllLookupsFilter does not work on related fields

In Django 1.9+, using AllLookupsFilter on a related field will use the incorrect set of lookups. It seems to use the lookups of the underlying AutoField instead of the lookups for the ForeignKey. For example, changing the test NoteFilterWIthAll FilterSet to:

class NoteFilterWithAll(FilterSet):
    title = AllLookupsFilter(name='title')
    author = AllLookupsFilter()

    class Meta:
        model = Note

Raises this error:

ImportError: Failed to import test module: tests.test_filterset
Traceback (most recent call last):
  File "lib/python2.7/unittest/loader.py", line 254, in _find_tests
    module = self._get_module_from_name(name)
  File "lib/python2.7/unittest/loader.py", line 232, in _get_module_from_name
    __import__(name)
  File "tests/test_regressions.py", line 25, in <module>
    from .testapp.filters import (
  File "tests/testapp/filters.py", line 20, in <module>
    class NoteFilterWithAll(FilterSet):
  File "rest_framework_filters/filterset.py", line 67, in __new__
    f = new_class.filter_for_field(field, filter_.name, lookup_expr)
  File ".env/lib/python2.7/site-packages/django_filters/filterset.py", line 422, in filter_for_field
    f, lookup_type = resolve_field(f, lookup_expr)
  File ".env/lib/python2.7/site-packages/django_filters/utils.py", line 87, in resolve_field
    final_lookup = lhs.get_lookup(name)
  File ".env/lib/python2.7/site-packages/django/db/models/expressions.py", line 291, in get_lookup
    return self.output_field.get_lookup(lookup)
  File ".env/lib/python2.7/site-packages/django/db/models/fields/related.py", line 693, in get_lookup
    raise TypeError('Related Field got invalid lookup: %s' % lookup_name)
TypeError: Related Field got invalid lookup: regex

Support filtering on strings with __in filters

The current __in filter supports only filtering on integers. There are several cases where I'd like to use an __in filter on strings. For example, when looking up several users through the username field.

Python 3.5 OrderedDict implementation breaks django-rest-framework-filters

Python 3.5 introduces a new implementation of OrderedDict in C.

A test in my project started failing after I upgraded from python 3.4 to 3.5 with RuntimeError: OrderedDict mutated during iteration, so I decided to clone django-rest-framework-filters and run the tests using python 3.5.

Six of them failed with the same RuntimeError. Some other tests had failed assertions. When I run the tests on the same machine but with Python 3.4 all the tests pass.

AllLookupsFilter does not work w/ FilterSet mixins

Hi !

cf https://docs.djangoproject.com/en/1.9/topics/db/models/#abstract-base-classes

Is there an equivalent for FilterSets ?

When I do:

class FilterMixin(FilterSet):
    common_field = AllLookupsFilter('field_name')

   # no possibility to do:
   # class Meta:
   #    abstract = True

class ActualFilter(FilterMixin, FilterSet):
    # other filters
    class Meta:
        model = MyModel

I get an error :

  File "rest_framework_filters/filterset.py", line 57, in __new__
    field = filterset.get_model_field(model, filter_.name)

If I do class FilterMixin(object): .., the common filters are not detected.

I would really like not having to repeat the fields multiple times !

Thanks !

A way to run .exclude instead of .filter

In the ORM you could do:

>>> Beer.objects.exclude(vendor__name__icontains='Busch').filter(...)

It would be awesome to replicate this functionality here. Perhaps prefacing the filter with __exclude ie:

http://some.serv.e.r/api/beers/?exclude__name__icontains='bud'

django-rest-framework-filters not compatible with django-filter 0.10.0

Django==1.8.3
djangorestframework-filters==0.5.0
django-filter==0.10.0
python Version 2.7.6

I added the library to my INSTALLED APPS but when i try to run the app , it throws me the following error:

Traceback (most recent call last):
  File "manage.py", line 13, in <module>
    execute_from_command_line(sys.argv)
  File "/Users/ronsuez/.virtualenvs/lims/lib/python2.7/site-packages/django/core/management/__init__.py", line 338, in execute_from_command_line
    utility.execute()
  File "/Users/ronsuez/.virtualenvs/lims/lib/python2.7/site-packages/django/core/management/__init__.py", line 312, in execute
    django.setup()
  File "/Users/ronsuez/.virtualenvs/lims/lib/python2.7/site-packages/django/__init__.py", line 18, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/Users/ronsuez/.virtualenvs/lims/lib/python2.7/site-packages/django/apps/registry.py", line 85, in populate
    app_config = AppConfig.create(entry)
  File "/Users/ronsuez/.virtualenvs/lims/lib/python2.7/site-packages/django/apps/config.py", line 86, in create
    module = import_module(entry)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
  File "/Users/ronsuez/.virtualenvs/lims/lib/python2.7/site-packages/rest_framework_filters/__init__.py", line 3, in <module>
    from .filterset import FilterSet
  File "/Users/ronsuez/.virtualenvs/lims/lib/python2.7/site-packages/rest_framework_filters/filterset.py", line 52, in <module>
    class FilterSet(six.with_metaclass(FilterSetMetaclass, filterset.FilterSet)):
  File "/Users/ronsuez/.virtualenvs/lims/lib/python2.7/site-packages/rest_framework_filters/filterset.py", line 58, in FilterSet
    'filter_class': filters.IsoDateTimeFilter,
AttributeError: 'module' object has no attribute 'IsoDateTimeFilter'

was searching in the django-filter-docs and they have that filter defined , but the package cannot found it

Complex filters?

How complex can our filters be?

Say I have a Post model:

class Post:
    user = models.ForeignKey(User, related_name='posts')
    title = models.CharField(max_length=200)

I want to find users who have an old post about gmail AND a new post about ymail

uo = User.objects
uo= uo.filter(posts__title__icontains="gmail", posts__publish_date__lte="01/01/2009")
uo= uo.filter(posts__title__icontains="ymail", posts__publish_date__gte="01/01/2015")

I don't see how I could currently do this with DRFF? I'd actually like to be able to do much more complex filters too

Query: Abstract filterset

I was wondering whether there's a way to apply a FilterSet to a non-generic API View and Django model - i.e. to just apply parsing of GET based filters?

e.g.

# View
class AbstractAPIView(APIView):
    def get(self, request, *args, **kwargs):
        filters = self.get_filters(self.request.query_params)
        custom_api.get_non_model_instances(from_date=filters.from_date)


# Filterset
class AbstractFilterset(filters.Filterset):
    from_date = filters.DateFilter()

# /endpoint?from_date=2016-01-01

Where the filterset could just be used to parse the query params, but not be applied to a queryset?

Set minimum Django version in setup.py

Traceback (most recent call last):
File "", line 1, in
File "/usr/local/lib/python2.7/site-packages/rest_framework_filters/init.py", line 3, in
from .filterset import FilterSet
File "/usr/local/lib/python2.7/site-packages/rest_framework_filters/filterset.py", line 13, in
from django_filters import filterset
File "/usr/local/lib/python2.7/site-packages/django_filters/init.py", line 3, in
from .filterset import FilterSet
File "/usr/local/lib/python2.7/site-packages/django_filters/filterset.py", line 19, in
from .filters import (Filter, CharFilter, BooleanFilter, BaseInFilter, BaseRangeFilter,
File "/usr/local/lib/python2.7/site-packages/django_filters/filters.py", line 16, in
from .fields import (
File "/usr/local/lib/python2.7/site-packages/django_filters/fields.py", line 13, in
from .utils import handle_timezone
File "/usr/local/lib/python2.7/site-packages/django_filters/utils.py", line 4, in
from django.db.models.expressions import Expression
ImportError: cannot import name Expression

Just an FYI - I couldn't find any django version restrictions in your release notes.

Cheers.

Add RelatedAllFilter?

A current limitation is that you cannot use AllLookupsFilter in conjunction with RelatedFilter. The following obviously does not work, as the declared filters will overwrite each other:

class FooFilter(FilterSet):
    bar = RelatedFilter()
    bar = AllLookupsFilter()

    class Meta:
        model = Foo

You cannot use the '__all__' shortcut for the dict-style syntax either, as it's just a shorthand for the declarative style above.
edit: the next release of django-filter will allow '__all__' to be reimplemented, solving a couple of issues. This still doesn't fix the declarative case though.

A workaround is to use the dict-style syntax without '__all__'. eg,

class FooFilter(FilterSet):
    bar = RelatedFilter()

    class Meta:
        model = Foo
        fields = {
            'bar': ['exact', 'lt', 'gt'],
        }

Including 'exact' does not matter here, as generated filters will not overwrite declared filters (which would be the RelatedFilter in this case.

We could add a RelatedAllFilter, AllLookupsRelatedFilter, AllLookupsWithRelatedFilter, or something along those lines that combines the behavior of both AllLookupsFilter and RelatedFilter.

__icontains filter doesn't work?

The exact search i.e ?title=django would give the right result. But whenever I use title__icontains or title__startswith it would return the whole ListView as in it's like without any filter. Is anyone experiencing this issue? Using Django 1.9 and DRF 3.3.2.

Multiple exclusions not working

neither /myapi/articles/?tags__contains=Tag 1&tags__contains=Tag 2 nor /myapi/articles/?tags__icontains=Tag 1&tags__icontains=Tag 2 works

/myapi/articles/?tags__contains=Tag 1&tags__icontains=Tag 2 works however

lookup type `isnull` does not work

I tried with

class ContainerChainedFilter(ChainedFilterSet):
    estimate_time = AllLookupsFilter(name='estimate_time')
    step = AllLookupsFilter(name='step')

    class Meta:
        model = ContainerSession
        fields = ['estimate_time', 'step']

step work with step__gte, step__gt, step__lt, step__lte.... but not work with
step__isnull=True, step__isnull=False, step__isnull=1, step__isnull=0...
Every lookup type work except isnull

compatible with django_model_utils Choices

I am using @carljm's django_model_util Choices field. is this package compatible? i ask because i am trying to create a filter that uses the human-readable version of the choices field backing a model field, and can't get it to work. if it is compatible, i'd love a concrete example to follow. thanks...

DjangoFilterBackend AttributeError: 'NoneType' object has no attribute 'verbose_name'

Hi, I'm using django-rest-framework-filters in my project, and it works quite well for some of my models which has FK & M2M. However, it doesn't work for some other models and I've posted a question on stackoverflow:
http://stackoverflow.com/questions/28247920/djangofilterbackend-not-working-with-my-models-manytomany
This seems quite a strange error. Many thanks!

This turns out to be a simple solution. thanks!

Support `__in` for field with `choices` defined.

The lookup type in is supported for CharField and NumberFiled though if a choices argument is passed to the field it won't work.

See fix_filter_field is using a condition on the type of f

    @classmethod
    def fix_filter_field(cls, f):
        """
        Fix the filter field based on the lookup type.
        """

        lookup_type = f.lookup_type
        if lookup_type == 'isnull':
            return filters.BooleanFilter(name=f.name, lookup_type='isnull')
        if lookup_type == 'in' and type(f) == filters.NumberFilter:
            return filters.InSetNumberFilter(name=f.name, lookup_type='in')
        if lookup_type == 'in' and type(f) == filters.CharFilter:
            return filters.InSetCharFilter(name=f.name, lookup_type='in')
        return f

Though if the field is provided with a choices attribute, it won't work. see https://github.com/alex/django-filter/blob/69767e12d0265fc4d0286db80aec88acd8783cf9/django_filters/filterset.py#L444 where it gets set to ChoiceFilter instead.

    if f.choices:
        default['choices'] = f.choices
        return ChoiceFilter(**default)

Right now the work around we are using is

class SomeFilter(filters.FilterSet):
    a_field = filters.AllLookupsFilter()
    a_field_in = filters.InSetCharFilter(name='a_field', lookup_type='in')

I have tried to play around a bit to find a fix but I could not come up with something sane in a timely matter. Maybe some one with more knowledge can weigh in and help out.

Feature request - Simplified custom lookup filter types

This makes the most sense applied to django-rest-framework-filter.

Creating custom lookup filters can be tedious with DRF (not using django-rest-framework-filter):

class SessionFilter(django_filters.FilterSet):
    client_type__contains = django_filters.CharFilter(name='client_type', lookup_type='contains')
    client_system__contains = django_filters.CharFilter(name='client_system', lookup_type='contains')

    starttime__gte = django_filters.DateTimeFilter(name='starttime', lookup_type='gte')
    starttime__lte = django_filters.DateTimeFilter(name='starttime', lookup_type='lte')
    starttime__range = django_filters.DateRangeFilter(name='starttime')
    class Meta:
        model = Session
        fields = ['client_type',
                  'client_system',
                  'starttime',
                  'project',
                  'language',
                  ]

class SessionList(generics.ListAPIView):
    queryset = Session.objects.all()
    serializer_class = SessionSerializer
    filter_class = SessionFilter

Here's the same thing with TastyPie:

class SessionResource(ModelResource):
    class Meta:
        queryset = Session.objects.all()
        resource_name = "session"
        allowed_methods = ['get']
        serializer = JSONSerializer()
        filtering = {
            "client_type": ('exact', 'contains',),
            "client_system": ('exact', 'contains'),
            "starttime": ('exact', 'gte', 'lte', 'range'),
            "project": ('exact'),
            "language": ('exact'),
        }

It would be great if a filtering dictionary were similarly optionally supported with DRF. Maybe this should be an issue on DRF? I'm not sure.

Thanks!

Using django_filters BooleanWidget breaks the rest framework browsable API

Because it does not implement the render method. I also reported the issue to django-filters: carltongibson/django-filter#383

I see that the BooleanWidget was introduced to fix issue #25 by commit d3fd92b but I'm unable to reproduce issue #25 when using django.forms.BooleanField with its the default widget CheckboxInput (ie. querying ?is_active=True, ?is_active=false, etc. works just fine, on the contrary it's using 0 or 1 that doesn't work as it evaluates to bool("0") or bool("1") which is always true. So we cool just get rid of the BooleanWidget if we're ok with just using True of False string in queries.

A better solution would be to have a BooleanWidget inheriting from both django.forms.widget.CheckboxInput for the render method and rest_framework.fields.BooleanField for the value_from_datadict method logic:

from rest_framework.fields import BooleanField
from django.forms.widget import CheckboxInput

class BooleanWidget(BooleanField, CheckboxInput):
    def value_from_datadict(self, data, files, name):
        if name not in data:
            # A missing value means False because HTML form submission does not
            # send results for unselected checkboxes.
            return False
        value = data.get(name)
        return self.to_internal_value(value)

And this widget coundn't be in django-filters as it doesn't depend on the rest framework but here in django-rest-framework-filters.

I'm using:

Django==1.8.9
django-filter==0.12.0
djangorestframework==3.3.2
djangorestframework-filters==0.7.0

ManyToMany Filters Duplicated Query

Hello,

Imagine this very simple example of Dogs that have Human enemies and Human friends:

model Human:

    name: CharField()

model Dog:

    friends:  ManyToMany(Human)
    enemies: ManytToMany(Human)

filter DogFilter:

    friends: Filter(name="friends")
    enemies: Filter(name="ennemies")

When I hit the API the filter class performs a duplicated query select all Humans one time for the field friends another time for the field enemies.

I have two questions:

1. is it possible to cache a duplicated query?
2. Why does these queries happen even when I do not filter at all in the API?

Let me know if you need more clarification.

neither contains or icontains is working

My /api/articles/ returns 100 results.
Both /api/articles/title__contains=Hot and /api/articles/title__icontains return 100 results.
Negation is working.
Exact match is working.
My ViewSet is as follows:

import rest_framework_filters as rf_filters
from rest_framework import filters

class ArticleFilterRf(rf_filters.FilterSet):
    category = rf_filters.CharFilter(name="cat__slug")
    category_id = rf_filters.CharFilter(name="cat_id")

    class Meta:
        model = Article
        fields = ['id', 'status', 'category', 'category_id', 'deleted', 'author', 'featured', 'tags', 'title']
class ArticleViewSet(viewsets.ModelViewSet):
    """
    A viewset for viewing and editing user instances.
    """
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
    serializer_class = ArticleSerializer
    queryset = Article.objects.all()
    filter_backends = (filters.SearchFilter, filters.DjangoFilterBackend, filters.OrderingFilter,)
    search_fields = ('title', 'tags', 'slug', 'status', 'cat__title', 'cat__slug', 'author')
    filter_class = ArticleFilterRf
    ordering_fields = ('title', 'updated')
    ordering = ('updated',)

isnull lookup got broken

Hello,

After upgrading the library from 0.3.1 to 0.6.0 isnull lookup got broken.

Models:

class AcceptanceCriteria(models.Model):
    name = models.CharField(max_length=40, primary_key=True)
    tests = models.ManyToManyField(Test)

class Test(models.Model):
    name = models.CharField(max_length=40)

Filterset:

class TestFilter(filters.FilterSet):
    name = filters.AllLookupsFilter(name='name')
    acceptancecriteria = filters.AllLookupsFilter(name="acceptancecriteria__name", distinct=True)

    class Meta:
        model = Test

Thrown exception is: Unsupported lookup 'isnull' for CharField or join on the field not permitted.
There were some other issues with previous version of library that were presumably solved in 0.6, so I've updated it and came across that problem. Could you help?

Regards,
Piotr

'range' lookup types do not work

Can you show an example that uses fieldname__range? After trying some things myself, googling and browsing source code of this project and django-filter I've come up empty handed.

Boolean filters don't work as expected

Django REST Framework says that When using boolean fields, you should use the values True and False in the URL query parameters, rather than 0, 1, true or false.

This cannot be achieved with django-rest-framework-filters. Must use 0 or 1 for boolean field filtering. True and False doesn't work.

Support for Django REST framework 3.2

Hello.

Just tried to upgrade to Django REST framework 3.2, which was released a couple of minutes ago, and this package is the first one that broke. The problem is in the usage of the deprecated request.QUERY_PARAMS instead of the new request.query_params. I can write some compat code, but need to know what versions of DRF are officially supported by this package. Most importantly if you plan to support below 3.0.

Maybe adding a tox run with the supported versions would also be good to get a firmer build matrix in place.

__all__ doesn't work on second query

It seems like this test case should pass:

diff --git a/tests/test_backends.py b/tests/test_backends.py
index 1394e3a..88fb8d3 100644
--- a/tests/test_backends.py
+++ b/tests/test_backends.py
@@ -16,3 +16,14 @@ class BackendTest(APITestCase):

         self.assertEqual(len(response.data), 1)
         self.assertEqual(response.data[0]['username'], 'user1')
+
+    def test_django_filter_compatibility_twice(self):
+        response = self.client.get('/dffield-users/', {'username': 'user1'}, content_type='json')
+
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['username'], 'user1')
+
+        response = self.client.get('/dffield-users/', {'username': 'user1'}, content_type='json')
+
+        self.assertEqual(len(response.data), 1)
+        self.assertEqual(response.data[0]['username'], 'user1')
diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py
index 23e87ba..5f7b405 100644
--- a/tests/testapp/urls.py
+++ b/tests/testapp/urls.py
@@ -7,6 +7,7 @@ from . import views

 router = routers.DefaultRouter()
 router.register(r'df-users', views.DFUserViewSet, base_name='df-users')
+router.register(r'dffield-users', views.DFFieldUserViewSet, base_name='dffield-users')
 router.register(r'users', views.UserViewSet,)
 router.register(r'notes', views.NoteViewSet,)

diff --git a/tests/testapp/views.py b/tests/testapp/views.py
index 7e3956f..75761a0 100644
--- a/tests/testapp/views.py
+++ b/tests/testapp/views.py
@@ -16,6 +16,17 @@ class DFUserViewSet(viewsets.ModelViewSet):
     filter_class = DFUserFilter


+class DFFieldUserViewSet(viewsets.ModelViewSet):
+    # used to test compatibility with the drf-filters backend
+    # with standard django-filter FilterSets.
+    queryset = User.objects.all()
+    serializer_class = UserSerializer
+    filter_backends = (backends.DjangoFilterBackend, )
+    filter_fields = {
+        'username': '__all__'
+    }
+
+
 class UserViewSet(viewsets.ModelViewSet):
     queryset = User.objects.all()
     serializer_class = UserSerializer

But it fails on the second get. It looks like the generation of fields runs through and deletes the Meta.fields which is a reference to the underlying filter_fields which are then deleted.

On the second API call, the get_filter_class will return None because there are no filter_fields or filter_class causing no filtering to be applied.

Exception on filtering related filters with non-existent field

Attempting to filter on a non-existent field of a related filter will cause cache_key() method to attempt to sort on a NoneType value causing an exception.

Problematic line here

Example stack trace:

Traceback:
File "/usr/local/lib/python3.4/dist-packages/django/core/handlers/base.py" in get_response
  132.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.4/dist-packages/django/views/decorators/csrf.py" in wrapped_view
  58.         return view_func(*args, **kwargs)
File "/usr/local/lib/python3.4/dist-packages/rest_framework/viewsets.py" in view
  87.             return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.4/dist-packages/rest_framework/views.py" in dispatch
  466.             response = self.handle_exception(exc)
File "/usr/local/lib/python3.4/dist-packages/rest_framework/views.py" in dispatch
  463.             response = handler(request, *args, **kwargs)
File "/usr/local/lib/python3.4/dist-packages/rest_framework_expander/views.py" in list
  53.         queryset = self.filter_queryset(self.get_queryset())
File "/usr/local/lib/python3.4/dist-packages/rest_framework/generics.py" in filter_queryset
  151.             queryset = backend().filter_queryset(self.request, queryset, self)
File "/usr/local/lib/python3.4/dist-packages/rest_framework_filters/backends.py" in filter_queryset
  15.             return filter_class(request.query_params, queryset=queryset, cache=cache).qs
File "/usr/local/lib/python3.4/dist-packages/rest_framework_filters/filterset.py" in qs
  174.         requested_filters = self.get_filters()
File "/usr/local/lib/python3.4/dist-packages/rest_framework_filters/filterset.py" in get_filters
  124.             key = self.cache_key(filterset_class, filter_names)
File "/usr/local/lib/python3.4/dist-packages/rest_framework_filters/filterset.py" in cache_key
  163.         return '%sSubset-%s' % (filterset.__name__, '-'.join(sorted(filter_names)), )

Exception Type: TypeError at /v0.7/posts/
Exception Value: sequence item 0: expected str instance, NoneType found

Traceability

Is there any documentation of the library with a full changelog between releases?

#363 tracking and cleanup

For 'order_by' deprecation details :

The warning message is incorrect. Please refer to #72 instead.


The following issues should be merged once carltongibson/django-filter#363 is merged/released.

The above PR fixes longstanding issues with filter generation for various lookup types ('isnull', 'in', 'range'), as well as the newly introduced lookup transforms.

TODO:

  • finish 363
  • wait for next release
  • remove fix_field_filter, as it should no longer be necessary
  • deprecate InSetNumberFilter/InSetCharFilter
  • fix AllLookupsFilter handling

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.