diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3ac74031e..ea87f235a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,6 @@ --- name: Bug report -about: Updating to 2.0? Did you read the migration guide? https://django-filter.readthedocs.io/en/master/guide/migration.html#migration-guide - +about: Found an issue, here's the place. Got a question? Use Discussions instead. --- diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d4c1871e6..9882636bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,11 @@ --- name: Tests -on: [push, pull_request] + +on: + push: + branches: + - main + pull_request: jobs: tests: @@ -8,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9.0-rc - 3.9] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 @@ -21,12 +26,12 @@ jobs: python -m pip install --upgrade pip setuptools - name: Install dependencies run: | - python -m pip install coverage tox tox-factor unittest-xml-reporting + python -m pip install coverage tox tox-py unittest-xml-reporting - name: Run tox run: | python -m pip --version python -m tox --version - python -m tox -f py$(python --version 2>&1 | cut -c 8,10) + python -m tox --py current - name: Coverage reporting run: | coverage combine @@ -44,7 +49,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: "3.10" - name: Ensure latest setuptools run: | python -m pip install --upgrade pip setuptools @@ -65,7 +70,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: "3.10" - name: Ensure latest setuptools run: | python -m pip install --upgrade pip setuptools diff --git a/.gitignore b/.gitignore index 98370680f..9636ffc35 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ docs/_build .coverage .coverage.* .xmlcoverage/ -.venv/ \ No newline at end of file +.venv/ +.idea +.env +.vscode diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..e92e23123 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.10" + +python: + install: + - requirements: requirements/docs.txt diff --git a/CHANGES.rst b/CHANGES.rst index 8d6e8de73..2ab9b15d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,104 @@ +Version 23.5 (2023-12-05) +------------------------- + +* Fixed OrderingFilter handling of empty values. (#1628) + + Thanks to Matt Munns. + +Version 23.4 (2023-11-20) +------------------------- + +* Official support for Django 5.0 and Python 3.12. + +* Fix DeprecationWarning for pkgutil.find_loader. + + Thanks to `wmorrell`. + +* Adopted Furo theme for docs. + +Version 23.3 (2023-9-17) +------------------------ + +* Adds initial compatibility with Django 5.0, prior to Django 5.0a1. + +* Updates packaging to use pyproject.toml and Flit. + +Version 23.2 (2023-4-30) +------------------------ + +* Deprecated the schema generation methods of the DRF related ``DjangoFilterBackend``. + These will be removed in version 25.1. + + You should use `drf-spectacular `_ + for generating OpenAPI schemas with DRF. + +* In addition, stopped testing against the (very old now) ``coreapi`` schema generation. + These methods should continue to work if you're using them until v25.1, but + ``coreapi`` is no longer maintained, and is raising warnings against the current + versions of Python. To workaround this is not worth the effort at this point. + +* Updated Polish translations. + +Version 23.1 (2023-3-26) +------------------------ + +* Declared support for Django 4.2. + +* Various updated and new translations. Thanks to all who contributed, and + Weblate for hosting. + +* Fixed QueryArrayWidget.value_from_datadict() to not mutate input data. (#1540) + +Version 22.1 (2022-6-17) +------------------------ + +* Update supported Python and Django versions: minimal Python is now 3.7, + minimum Django is now 3.2. + +* Added testing for Python 3.10 and Django 4.1. + +* Removed outdated deprecated warnings for code removed in version 2.1. + +* Removed `filter_class` (use `filterset_class`) and `filter_fields` + (`filterset_fields`) that were deprecated in [version 2.0 + (2018)](https://django-filter.readthedocs.io/en/main/guide/migration.html#view-attributes-renamed-867). + +* The code base is now formatted with Black. + +Version 21.1 (2021-9-24) +------------------------ + +This is a maintenance release updating CI testing for the latest +non-end-of-life versions of Python and Django, and updating package metadata +accordingly. + +With this release ``django-filter`` is switching to a two-part CalVer +versioning scheme, such as ``21.1``. The first number is the year. The second +is the release number within that year. + +On an on-going basis, Django-Filter aims to support all current Django +versions, the matching current Python versions, and the latest version of +Django REST Framework. + +Please see: + +* `Status of supported Python branches `_ +* `List of supported Django versions `_ + +Support for Python and Django versions will be dropped when they reach +end-of-life. Support for Python versions will dropped when they reach +end-of-life, even when still supported by a current version of Django. + +Other breaking changes are rare. Where required, every effort will be made to +apply a "Year plus two" deprecation period. For example, a change initially +introduced in ``23.x`` would offer a fallback where feasible and finally be +removed in ``25.1``. Where fallbacks are not feasible, breaking changes without +deprecation will be called out in the release notes. + +Beyond that change, there are few changes. Some small bugfixes, improvements to +localisation, and documentation tweaks. Thanks to all who were involved. + + Version 2.4.0 (2020-9-27) -------------------------- @@ -15,6 +116,7 @@ Version 2.4.0 (2020-9-27) In addition tests against Django main development branch are now required to pass. + Version 2.3.0 (2020-6-5) ------------------------ @@ -27,6 +129,7 @@ Version 2.3.0 (2020-6-5) * Fixed IsoDateTimeRangeFieldTests for Django 3.1 * Require tests to pass against Django `master`. + Version 2.2 (2019-7-16) ----------------------- @@ -56,7 +159,7 @@ Version 2.0 (2018-7-13) 2.0 introduced a number of small changes and tidy-ups. Please see the migration guide: -https://django-filter.readthedocs.io/en/master/guide/migration.html#migrating-to-2-0 +https://django-filter.readthedocs.io/en/main/guide/migration.html#migrating-to-2-0 * Added testing for Python 3.7 (#944) * Improve exception message for invalid filter result (#943) diff --git a/README.rst b/README.rst index 248221708..026df4948 100644 --- a/README.rst +++ b/README.rst @@ -12,15 +12,32 @@ Full documentation on `read the docs`_. .. image:: https://badge.fury.io/py/django-filter.svg :target: http://badge.fury.io/py/django-filter -Requirements ------------- -* **Python**: 3.5, 3.6, 3.7, 3.8, 3.9 -* **Django**: 2.2, 3.0, 3.1 -* **DRF**: 3.10+ +Versioning and stability policy +------------------------------- + +Django-Filter is a mature and stable package. It uses a two-part CalVer +versioning scheme, such as ``21.1``. The first number is the year. The second +is the release number within that year. + +On an on-going basis, Django-Filter aims to support all current Django +versions, the matching current Python versions, and the latest version of +Django REST Framework. + +Please see: + +* `Status of supported Python versions `_ +* `List of supported Django versions `_ + +Support for Python and Django versions will be dropped when they reach +end-of-life. Support for Python versions will be dropped when they reach +end-of-life, even when still supported by a current version of Django. -From Version 2.0 Django Filter is Python 3 only. -If you need to support Python 2.7 use the version 1.1 release. +Other breaking changes are rare. Where required, every effort will be made to +apply a "Year plus two" deprecation period. For example, a change initially +introduced in ``23.x`` would offer a fallback where feasible and finally be +removed in ``25.1``. Where fallbacks are not feasible, breaking changes without +deprecation will be called out in the release notes. Installation @@ -94,9 +111,9 @@ For more details see the `DRF integration docs`_. Support ------- -If you have questions about usage or development you can join the -`mailing list`_. +If you need help you can start a `discussion`_. For commercial support, please +`contact Carlton Gibson via his website `_. -.. _`read the docs`: https://django-filter.readthedocs.io/en/master/ -.. _`mailing list`: http://groups.google.com/group/django-filter -.. _`DRF integration docs`: https://django-filter.readthedocs.io/en/master/guide/rest_framework.html +.. _`discussion`: https://github.com/carltongibson/django-filter/discussions +.. _`read the docs`: https://django-filter.readthedocs.io/en/main/ +.. _`DRF integration docs`: https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html diff --git a/django_filters/__init__.py b/django_filters/__init__.py index 7828c53e0..5be169b51 100644 --- a/django_filters/__init__.py +++ b/django_filters/__init__.py @@ -1,24 +1,24 @@ # flake8: noqa -import pkgutil +from importlib import util as importlib_util from .filters import * from .filterset import FilterSet # We make the `rest_framework` module available without an additional import. # If DRF is not installed, no-op. -if pkgutil.find_loader('rest_framework') is not None: +if importlib_util.find_spec("rest_framework"): from . import rest_framework -del pkgutil +del importlib_util -__version__ = '2.4.0' +__version__ = "23.5" def parse_version(version): - ''' + """ '0.1.2.dev1' -> (0, 1, 2, 'dev1') '0.1.2' -> (0, 1, 2) - ''' - v = version.split('.') + """ + v = version.split(".") ret = [] for p in v: if p.isdigit(): @@ -27,4 +27,13 @@ def parse_version(version): ret.append(p) return tuple(ret) + VERSION = parse_version(__version__) + + + +assert VERSION < (25,0), "Remove deprecated code" + + +class RemovedInDjangoFilter25Warning(DeprecationWarning): + pass diff --git a/django_filters/compat.py b/django_filters/compat.py index fcaa9e9b4..801ddcd42 100644 --- a/django_filters/compat.py +++ b/django_filters/compat.py @@ -1,4 +1,11 @@ +import django from django.conf import settings +from django.test import TestCase + +if django.VERSION < (4, 2): + class TestCase(TestCase): + assertQuerySetEqual = TestCase.assertQuerysetEqual + # django-crispy-forms is optional try: @@ -8,7 +15,7 @@ def is_crispy(): - return 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms + return "crispy_forms" in settings.INSTALLED_APPS and crispy_forms # coreapi is optional (Note that uritemplate is a dependency of coreapi) diff --git a/django_filters/conf.py b/django_filters/conf.py index 64a68c641..cbef0b76c 100644 --- a/django_filters/conf.py +++ b/django_filters/conf.py @@ -5,59 +5,53 @@ from .utils import deprecate DEFAULTS = { - 'DISABLE_HELP_TEXT': False, - - 'DEFAULT_LOOKUP_EXPR': 'exact', - + "DISABLE_HELP_TEXT": False, + "DEFAULT_LOOKUP_EXPR": "exact", # empty/null choices - 'EMPTY_CHOICE_LABEL': '---------', - 'NULL_CHOICE_LABEL': None, - 'NULL_CHOICE_VALUE': 'null', - - 'VERBOSE_LOOKUPS': { + "EMPTY_CHOICE_LABEL": "---------", + "NULL_CHOICE_LABEL": None, + "NULL_CHOICE_VALUE": "null", + "VERBOSE_LOOKUPS": { # transforms don't need to be verbose, since their expressions are chained - 'date': _('date'), - 'year': _('year'), - 'month': _('month'), - 'day': _('day'), - 'week_day': _('week day'), - 'hour': _('hour'), - 'minute': _('minute'), - 'second': _('second'), - + "date": _("date"), + "year": _("year"), + "month": _("month"), + "day": _("day"), + "week_day": _("week day"), + "hour": _("hour"), + "minute": _("minute"), + "second": _("second"), # standard lookups - 'exact': _(''), - 'iexact': _(''), - 'contains': _('contains'), - 'icontains': _('contains'), - 'in': _('is in'), - 'gt': _('is greater than'), - 'gte': _('is greater than or equal to'), - 'lt': _('is less than'), - 'lte': _('is less than or equal to'), - 'startswith': _('starts with'), - 'istartswith': _('starts with'), - 'endswith': _('ends with'), - 'iendswith': _('ends with'), - 'range': _('is in range'), - 'isnull': _(''), - 'regex': _('matches regex'), - 'iregex': _('matches regex'), - 'search': _('search'), - + "exact": "", + "iexact": "", + "contains": _("contains"), + "icontains": _("contains"), + "in": _("is in"), + "gt": _("is greater than"), + "gte": _("is greater than or equal to"), + "lt": _("is less than"), + "lte": _("is less than or equal to"), + "startswith": _("starts with"), + "istartswith": _("starts with"), + "endswith": _("ends with"), + "iendswith": _("ends with"), + "range": _("is in range"), + "isnull": _("is null"), + "regex": _("matches regex"), + "iregex": _("matches regex"), + "search": _("search"), # postgres lookups - 'contained_by': _('is contained by'), - 'overlap': _('overlaps'), - 'has_key': _('has key'), - 'has_keys': _('has keys'), - 'has_any_keys': _('has any keys'), - 'trigram_similar': _('search'), + "contained_by": _("is contained by"), + "overlap": _("overlaps"), + "has_key": _("has key"), + "has_keys": _("has keys"), + "has_any_keys": _("has any keys"), + "trigram_similar": _("search"), }, } -DEPRECATED_SETTINGS = [ -] +DEPRECATED_SETTINGS = [] def is_callable(value): @@ -66,7 +60,6 @@ def is_callable(value): class Settings: - def __getattr__(self, name): if name not in DEFAULTS: msg = "'%s' object has no attribute '%s'" @@ -82,7 +75,7 @@ def __getattr__(self, name): return value def get_setting(self, setting): - django_setting = 'FILTERS_%s' % setting + django_setting = "FILTERS_%s" % setting if setting in DEPRECATED_SETTINGS and hasattr(dj_settings, django_setting): deprecate("The '%s' setting has been deprecated." % django_setting) @@ -90,7 +83,7 @@ def get_setting(self, setting): return getattr(dj_settings, django_setting, DEFAULTS[setting]) def change_setting(self, setting, value, enter, **kwargs): - if not setting.startswith('FILTERS_'): + if not setting.startswith("FILTERS_"): return setting = setting[8:] # strip 'FILTERS_' diff --git a/django_filters/constants.py b/django_filters/constants.py index 795d6ccca..93aa5b38a 100644 --- a/django_filters/constants.py +++ b/django_filters/constants.py @@ -1,5 +1,4 @@ +ALL_FIELDS = "__all__" -ALL_FIELDS = '__all__' - -EMPTY_VALUES = ([], (), {}, '', None) +EMPTY_VALUES = ([], (), {}, "", None) diff --git a/django_filters/exceptions.py b/django_filters/exceptions.py index 1d79e4df8..978bfb37e 100644 --- a/django_filters/exceptions.py +++ b/django_filters/exceptions.py @@ -1,4 +1,3 @@ - from django.core.exceptions import FieldError diff --git a/django_filters/fields.py b/django_filters/fields.py index 23f30f7a5..3a3436510 100644 --- a/django_filters/fields.py +++ b/django_filters/fields.py @@ -14,18 +14,23 @@ CSVWidget, DateRangeWidget, LookupChoiceWidget, - RangeWidget + RangeWidget, ) +try: + from django.utils.choices import BaseChoiceIterator, normalize_choices +except ImportError: + DJANGO_50 = False +else: + DJANGO_50 = True + class RangeField(forms.MultiValueField): widget = RangeWidget def __init__(self, fields=None, *args, **kwargs): if fields is None: - fields = ( - forms.DecimalField(), - forms.DecimalField()) + fields = (forms.DecimalField(), forms.DecimalField()) super().__init__(fields, *args, **kwargs) def compress(self, data_list): @@ -38,9 +43,7 @@ class DateRangeField(RangeField): widget = DateRangeWidget def __init__(self, *args, **kwargs): - fields = ( - forms.DateField(), - forms.DateField()) + fields = (forms.DateField(), forms.DateField()) super().__init__(fields, *args, **kwargs) def compress(self, data_list): @@ -48,13 +51,11 @@ def compress(self, data_list): start_date, stop_date = data_list if start_date: start_date = handle_timezone( - datetime.combine(start_date, time.min), - False + datetime.combine(start_date, time.min), False ) if stop_date: stop_date = handle_timezone( - datetime.combine(stop_date, time.max), - False + datetime.combine(stop_date, time.max), False ) return slice(start_date, stop_date) return None @@ -64,9 +65,7 @@ class DateTimeRangeField(RangeField): widget = DateRangeWidget def __init__(self, *args, **kwargs): - fields = ( - forms.DateTimeField(), - forms.DateTimeField()) + fields = (forms.DateTimeField(), forms.DateTimeField()) super().__init__(fields, *args, **kwargs) @@ -74,9 +73,7 @@ class IsoDateTimeRangeField(RangeField): widget = DateRangeWidget def __init__(self, *args, **kwargs): - fields = ( - IsoDateTimeField(), - IsoDateTimeField()) + fields = (IsoDateTimeField(), IsoDateTimeField()) super().__init__(fields, *args, **kwargs) @@ -84,13 +81,11 @@ class TimeRangeField(RangeField): widget = DateRangeWidget def __init__(self, *args, **kwargs): - fields = ( - forms.TimeField(), - forms.TimeField()) + fields = (forms.TimeField(), forms.TimeField()) super().__init__(fields, *args, **kwargs) -class Lookup(namedtuple('Lookup', ('value', 'lookup_expr'))): +class Lookup(namedtuple("Lookup", ("value", "lookup_expr"))): def __new__(cls, value, lookup_expr): if value in EMPTY_VALUES or lookup_expr in EMPTY_VALUES: raise ValueError( @@ -103,15 +98,15 @@ def __new__(cls, value, lookup_expr): class LookupChoiceField(forms.MultiValueField): default_error_messages = { - 'lookup_required': _('Select a lookup.'), + "lookup_required": _("Select a lookup."), } def __init__(self, field, lookup_choices, *args, **kwargs): - empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL) + empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL) fields = (field, ChoiceField(choices=lookup_choices, empty_label=empty_label)) widget = LookupChoiceWidget(widgets=[f.widget for f in fields]) - kwargs['widget'] = widget - kwargs['help_text'] = field.help_text + kwargs["widget"] = widget + kwargs["help_text"] = field.help_text super().__init__(fields, *args, **kwargs) def compress(self, data_list): @@ -122,8 +117,8 @@ def compress(self, data_list): return Lookup(value=value, lookup_expr=lookup_expr) else: raise forms.ValidationError( - self.error_messages['lookup_required'], - code='lookup_required') + self.error_messages["lookup_required"], code="lookup_required" + ) return None @@ -136,7 +131,8 @@ class IsoDateTimeField(forms.DateTimeField): Based on Gist example by David Medina https://gist.github.com/copitux/5773821 """ - ISO_8601 = 'iso-8601' + + ISO_8601 = "iso-8601" input_formats = [ISO_8601] def strptime(self, value, format): @@ -160,30 +156,42 @@ class IntegerCSVField(BaseCSVField, filters.IntegerField): pass """ + base_widget_class = BaseCSVWidget def __init__(self, *args, **kwargs): - widget = kwargs.get('widget') or self.widget - kwargs['widget'] = self._get_widget_class(widget) + widget = kwargs.get("widget") or self.widget + kwargs["widget"] = self._get_widget_class(widget) super().__init__(*args, **kwargs) def _get_widget_class(self, widget): # passthrough, allows for override if isinstance(widget, BaseCSVWidget) or ( - isinstance(widget, type) and - issubclass(widget, BaseCSVWidget)): + isinstance(widget, type) and issubclass(widget, BaseCSVWidget) + ): return widget # complain since we are unable to reconstruct widget instances - assert isinstance(widget, type), \ - "'%s.widget' must be a widget class, not %s." \ - % (self.__class__.__name__, repr(widget)) - - bases = (self.base_widget_class, widget, ) - return type(str('CSV%s' % widget.__name__), bases, {}) + assert isinstance( + widget, type + ), "'%s.widget' must be a widget class, not %s." % ( + self.__class__.__name__, + repr(widget), + ) + + bases = ( + self.base_widget_class, + widget, + ) + return type(str("CSV%s" % widget.__name__), bases, {}) def clean(self, value): + if value in self.empty_values and self.required: + raise forms.ValidationError( + self.error_messages["required"], code="required" + ) + if value is None: return None return [super(BaseCSVField, self).clean(v) for v in value] @@ -194,9 +202,7 @@ class BaseRangeField(BaseCSVField): # input would only allow a user to input one value and would always fail. widget = CSVWidget - default_error_messages = { - 'invalid_values': _('Range query expects two values.') - } + default_error_messages = {"invalid_values": _("Range query expects two values.")} def clean(self, value): value = super().clean(value) @@ -205,13 +211,13 @@ def clean(self, value): if value and len(value) != 2: raise forms.ValidationError( - self.error_messages['invalid_values'], - code='invalid_values') + self.error_messages["invalid_values"], code="invalid_values" + ) return value -class ChoiceIterator: +class ChoiceIterator(BaseChoiceIterator if DJANGO_50 else object): # Emulates the behavior of ModelChoiceIterator, but instead wraps # the field's _choices iterable. @@ -224,7 +230,10 @@ def __iter__(self): yield ("", self.field.empty_label) if self.field.null_label is not None: yield (self.field.null_value, self.field.null_label) - yield from self.choices + if DJANGO_50: + yield from normalize_choices(self.choices) + else: + yield from self.choices def __len__(self): add = 1 if self.field.empty_label is not None else 0 @@ -253,20 +262,26 @@ def __len__(self): class ChoiceIteratorMixin: def __init__(self, *args, **kwargs): - self.null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL) - self.null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE) + self.null_label = kwargs.pop("null_label", settings.NULL_CHOICE_LABEL) + self.null_value = kwargs.pop("null_value", settings.NULL_CHOICE_VALUE) super().__init__(*args, **kwargs) - def _get_choices(self): - return super()._get_choices() + @property + def choices(self): + return super().choices - def _set_choices(self, value): - super()._set_choices(value) - value = self.iterator(self, self._choices) + @choices.setter + def choices(self, value): + if DJANGO_50: + value = self.iterator(self, value) + else: + super()._set_choices(value) + value = self.iterator(self, self._choices) - self._choices = self.widget.choices = value - choices = property(_get_choices, _set_choices) + # Simple `super()` syntax for calling a parent property setter is + # unsupported. See https://github.com/python/cpython/issues/59170 + super(ChoiceIteratorMixin, self.__class__).choices.__set__(self, value) # Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label @@ -274,7 +289,7 @@ class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField): iterator = ChoiceIterator def __init__(self, *args, **kwargs): - self.empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL) + self.empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL) super().__init__(*args, **kwargs) diff --git a/django_filters/filters.py b/django_filters/filters.py index 8190af82e..f99e82cfe 100644 --- a/django_filters/filters.py +++ b/django_filters/filters.py @@ -1,5 +1,6 @@ from collections import OrderedDict from datetime import timedelta +from itertools import chain from django import forms from django.core.validators import MaxValueValidator @@ -25,41 +26,41 @@ ModelMultipleChoiceField, MultipleChoiceField, RangeField, - TimeRangeField + TimeRangeField, ) from .utils import get_model_field, label_for_filter __all__ = [ - 'AllValuesFilter', - 'AllValuesMultipleFilter', - 'BaseCSVFilter', - 'BaseInFilter', - 'BaseRangeFilter', - 'BooleanFilter', - 'CharFilter', - 'ChoiceFilter', - 'DateFilter', - 'DateFromToRangeFilter', - 'DateRangeFilter', - 'DateTimeFilter', - 'DateTimeFromToRangeFilter', - 'DurationFilter', - 'Filter', - 'IsoDateTimeFilter', - 'IsoDateTimeFromToRangeFilter', - 'LookupChoiceFilter', - 'ModelChoiceFilter', - 'ModelMultipleChoiceFilter', - 'MultipleChoiceFilter', - 'NumberFilter', - 'NumericRangeFilter', - 'OrderingFilter', - 'RangeFilter', - 'TimeFilter', - 'TimeRangeFilter', - 'TypedChoiceFilter', - 'TypedMultipleChoiceFilter', - 'UUIDFilter', + "AllValuesFilter", + "AllValuesMultipleFilter", + "BaseCSVFilter", + "BaseInFilter", + "BaseRangeFilter", + "BooleanFilter", + "CharFilter", + "ChoiceFilter", + "DateFilter", + "DateFromToRangeFilter", + "DateRangeFilter", + "DateTimeFilter", + "DateTimeFromToRangeFilter", + "DurationFilter", + "Filter", + "IsoDateTimeFilter", + "IsoDateTimeFromToRangeFilter", + "LookupChoiceFilter", + "ModelChoiceFilter", + "ModelMultipleChoiceFilter", + "MultipleChoiceFilter", + "NumberFilter", + "NumericRangeFilter", + "OrderingFilter", + "RangeFilter", + "TimeFilter", + "TimeRangeFilter", + "TypedChoiceFilter", + "TypedMultipleChoiceFilter", + "UUIDFilter", ] @@ -67,8 +68,17 @@ class Filter: creation_counter = 0 field_class = forms.Field - def __init__(self, field_name=None, lookup_expr=None, *, label=None, - method=None, distinct=False, exclude=False, **kwargs): + def __init__( + self, + field_name=None, + lookup_expr=None, + *, + label=None, + method=None, + distinct=False, + exclude=False, + **kwargs + ): if lookup_expr is None: lookup_expr = settings.DEFAULT_LOOKUP_EXPR self.field_name = field_name @@ -79,14 +89,14 @@ def __init__(self, field_name=None, lookup_expr=None, *, label=None, self.exclude = exclude self.extra = kwargs - self.extra.setdefault('required', False) + self.extra.setdefault("required", False) self.creation_counter = Filter.creation_counter Filter.creation_counter += 1 def get_method(self, qs): """Return filter method based on whether we're excluding - or simply filtering. + or simply filtering. """ return qs.exclude if self.exclude else qs.filter @@ -95,6 +105,7 @@ def method(): Filter method needs to be lazily resolved, as it may be dependent on the 'parent' FilterSet. """ + def fget(self): return self._method @@ -110,11 +121,12 @@ def fset(self, value): self.filter = FilterMethod(self) return locals() + method = property(**method()) def label(): def fget(self): - if self._label is None and hasattr(self, 'model'): + if self._label is None and hasattr(self, "model"): self._label = label_for_filter( self.model, self.field_name, self.lookup_expr, self.exclude ) @@ -124,15 +136,16 @@ def fset(self, value): self._label = value return locals() + label = property(**label()) @property def field(self): - if not hasattr(self, '_field'): + if not hasattr(self, "_field"): field_kwargs = self.extra.copy() if settings.DISABLE_HELP_TEXT: - field_kwargs.pop('help_text', None) + field_kwargs.pop("help_text", None) self._field = self.field_class(label=self.label, **field_kwargs) return self._field @@ -142,7 +155,7 @@ def filter(self, qs, value): return qs if self.distinct: qs = qs.distinct() - lookup = '%s__%s' % (self.field_name, self.lookup_expr) + lookup = "%s__%s" % (self.field_name, self.lookup_expr) qs = self.get_method(qs)(**{lookup: value}) return qs @@ -159,14 +172,16 @@ class ChoiceFilter(Filter): field_class = ChoiceField def __init__(self, *args, **kwargs): - self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE) + self.null_value = kwargs.get("null_value", settings.NULL_CHOICE_VALUE) super().__init__(*args, **kwargs) def filter(self, qs, value): if value != self.null_value: return super().filter(qs, value) - qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): None}) + qs = self.get_method(qs)( + **{"%s__%s" % (self.field_name, self.lookup_expr): None} + ) return qs.distinct() if self.distinct else qs @@ -201,14 +216,15 @@ class MultipleChoiceFilter(Filter): `distinct` defaults to `True` as to-many relationships will generally require this. """ + field_class = MultipleChoiceField always_filter = True def __init__(self, *args, **kwargs): - kwargs.setdefault('distinct', True) - self.conjoined = kwargs.pop('conjoined', False) - self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE) + kwargs.setdefault("distinct", True) + self.conjoined = kwargs.pop("conjoined", False) + self.null_value = kwargs.get("null_value", settings.NULL_CHOICE_VALUE) super().__init__(*args, **kwargs) def is_noop(self, qs, value): @@ -220,7 +236,7 @@ def is_noop(self, qs, value): return False # A reasonable default for being a noop... - if self.extra.get('required') and len(value) == len(self.field.choices): + if self.extra.get("required") and len(value) == len(self.field.choices): return True return False @@ -281,6 +297,7 @@ class IsoDateTimeFilter(DateTimeFilter): * https://github.com/encode/django-rest-framework/issues/1338 * https://github.com/carltongibson/django-filter/pull/264 """ + field_class = IsoDateTimeField @@ -315,8 +332,9 @@ class EmployeeFilter(filters.FilterSet): user's associated company. """ + def __init__(self, *args, **kwargs): - self.queryset = kwargs.get('queryset') + self.queryset = kwargs.get("queryset") super().__init__(*args, **kwargs) def get_request(self): @@ -338,7 +356,7 @@ def field(self): queryset = self.get_queryset(request) if queryset is not None: - self.extra['queryset'] = queryset + self.extra["queryset"] = queryset return super().field @@ -347,7 +365,7 @@ class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter): field_class = ModelChoiceField def __init__(self, *args, **kwargs): - kwargs.setdefault('empty_label', settings.EMPTY_CHOICE_LABEL) + kwargs.setdefault("empty_label", settings.EMPTY_CHOICE_LABEL) super().__init__(*args, **kwargs) @@ -366,7 +384,7 @@ def get_max_validator(self): @property def field(self): - if not hasattr(self, '_field'): + if not hasattr(self, "_field"): field = super().field max_validator = self.get_max_validator() if max_validator: @@ -384,10 +402,10 @@ def filter(self, qs, value): if value.start is not None and value.stop is not None: value = (value.start, value.stop) elif value.start is not None: - self.lookup_expr = 'startswith' + self.lookup_expr = "startswith" value = value.start elif value.stop is not None: - self.lookup_expr = 'endswith' + self.lookup_expr = "endswith" value = value.stop return super().filter(qs, value) @@ -399,13 +417,13 @@ class RangeFilter(Filter): def filter(self, qs, value): if value: if value.start is not None and value.stop is not None: - self.lookup_expr = 'range' + self.lookup_expr = "range" value = (value.start, value.stop) elif value.start is not None: - self.lookup_expr = 'gte' + self.lookup_expr = "gte" value = value.start elif value.stop is not None: - self.lookup_expr = 'lte' + self.lookup_expr = "lte" value = value.stop return super().filter(qs, value) @@ -417,35 +435,42 @@ def _truncate(dt): class DateRangeFilter(ChoiceFilter): choices = [ - ('today', _('Today')), - ('yesterday', _('Yesterday')), - ('week', _('Past 7 days')), - ('month', _('This month')), - ('year', _('This year')), + ("today", _("Today")), + ("yesterday", _("Yesterday")), + ("week", _("Past 7 days")), + ("month", _("This month")), + ("year", _("This year")), ] filters = { - 'today': lambda qs, name: qs.filter(**{ - '%s__year' % name: now().year, - '%s__month' % name: now().month, - '%s__day' % name: now().day - }), - 'yesterday': lambda qs, name: qs.filter(**{ - '%s__year' % name: (now() - timedelta(days=1)).year, - '%s__month' % name: (now() - timedelta(days=1)).month, - '%s__day' % name: (now() - timedelta(days=1)).day, - }), - 'week': lambda qs, name: qs.filter(**{ - '%s__gte' % name: _truncate(now() - timedelta(days=7)), - '%s__lt' % name: _truncate(now() + timedelta(days=1)), - }), - 'month': lambda qs, name: qs.filter(**{ - '%s__year' % name: now().year, - '%s__month' % name: now().month - }), - 'year': lambda qs, name: qs.filter(**{ - '%s__year' % name: now().year, - }), + "today": lambda qs, name: qs.filter( + **{ + "%s__year" % name: now().year, + "%s__month" % name: now().month, + "%s__day" % name: now().day, + } + ), + "yesterday": lambda qs, name: qs.filter( + **{ + "%s__year" % name: (now() - timedelta(days=1)).year, + "%s__month" % name: (now() - timedelta(days=1)).month, + "%s__day" % name: (now() - timedelta(days=1)).day, + } + ), + "week": lambda qs, name: qs.filter( + **{ + "%s__gte" % name: _truncate(now() - timedelta(days=7)), + "%s__lt" % name: _truncate(now() + timedelta(days=1)), + } + ), + "month": lambda qs, name: qs.filter( + **{"%s__year" % name: now().year, "%s__month" % name: now().month} + ), + "year": lambda qs, name: qs.filter( + **{ + "%s__year" % name: now().year, + } + ), } def __init__(self, choices=None, filters=None, *args, **kwargs): @@ -454,18 +479,22 @@ def __init__(self, choices=None, filters=None, *args, **kwargs): if filters is not None: self.filters = filters - unique = set([x[0] for x in self.choices]) ^ set(self.filters) - assert not unique, \ - "Keys must be present in both 'choices' and 'filters'. Missing keys: " \ - "'%s'" % ', '.join(sorted(unique)) - - # TODO: remove assertion in 2.1 - assert not hasattr(self, 'options'), \ - "The 'options' attribute has been replaced by 'choices' and 'filters'. " \ - "See: https://django-filter.readthedocs.io/en/master/guide/migration.html" + all_choices = list( + chain.from_iterable( + [subchoice[0] for subchoice in choice[1]] + if isinstance(choice[1], (list, tuple)) # This is an optgroup + else [choice[0]] + for choice in self.choices + ) + ) + unique = set(all_choices) ^ set(self.filters) + assert not unique, ( + "Keys must be present in both 'choices' and 'filters'. Missing keys: " + "'%s'" % ", ".join(sorted(unique)) + ) # null choice not relevant - kwargs.setdefault('null_label', None) + kwargs.setdefault("null_label", None) super().__init__(choices=self.choices, *args, **kwargs) def filter(self, qs, value): @@ -499,7 +528,7 @@ class AllValuesFilter(ChoiceFilter): def field(self): qs = self.model._default_manager.distinct() qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True) - self.extra['choices'] = [(o, o) for o in qs] + self.extra["choices"] = [(o, o) for o in qs] return super().field @@ -508,7 +537,7 @@ class AllValuesMultipleFilter(MultipleChoiceFilter): def field(self): qs = self.model._default_manager.distinct() qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True) - self.extra['choices'] = [(o, o) for o in qs] + self.extra["choices"] = [(o, o) for o in qs] return super().field @@ -516,14 +545,16 @@ class BaseCSVFilter(Filter): """ Base class for CSV type filters, such as IN and RANGE. """ + base_field_class = BaseCSVField def __init__(self, *args, **kwargs): - kwargs.setdefault('help_text', _('Multiple values may be separated by commas.')) + kwargs.setdefault("help_text", _("Multiple values may be separated by commas.")) super().__init__(*args, **kwargs) class ConcreteCSVField(self.base_field_class, self.field_class): pass + ConcreteCSVField.__name__ = self._field_class_name( self.field_class, self.lookup_expr ) @@ -546,21 +577,20 @@ def _field_class_name(cls, field_class, lookup_expr): """ # DateTimeField => DateTime type_name = field_class.__name__ - if type_name.endswith('Field'): + if type_name.endswith("Field"): type_name = type_name[:-5] # year__in => YearIn parts = lookup_expr.split(LOOKUP_SEP) - expression_name = ''.join(p.capitalize() for p in parts) + expression_name = "".join(p.capitalize() for p in parts) # DateTimeYearInField - return str('%s%sField' % (type_name, expression_name)) + return str("%s%sField" % (type_name, expression_name)) class BaseInFilter(BaseCSVFilter): - def __init__(self, *args, **kwargs): - kwargs.setdefault('lookup_expr', 'in') + kwargs.setdefault("lookup_expr", "in") super().__init__(*args, **kwargs) @@ -568,7 +598,7 @@ class BaseRangeFilter(BaseCSVFilter): base_field_class = BaseRangeField def __init__(self, *args, **kwargs): - kwargs.setdefault('lookup_expr', 'range') + kwargs.setdefault("lookup_expr", "range") super().__init__(*args, **kwargs) @@ -577,7 +607,7 @@ class LookupChoiceFilter(Filter): A combined filter that allows users to select the lookup expression from a dropdown. * ``lookup_choices`` is an optional argument that accepts multiple input - formats, and is ultimately normlized as the choices used in the lookup + formats, and is ultimately normalized as the choices used in the lookup dropdown. See ``.get_lookup_choices()`` for more information. * ``field_class`` is an optional argument that allows you to set the inner @@ -595,11 +625,14 @@ class LookupChoiceFilter(Filter): ) """ + field_class = forms.CharField outer_class = LookupChoiceField - def __init__(self, field_name=None, lookup_choices=None, field_class=None, **kwargs): - self.empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL) + def __init__( + self, field_name=None, lookup_choices=None, field_class=None, **kwargs + ): + self.empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL) super(LookupChoiceFilter, self).__init__(field_name=field_name, **kwargs) @@ -645,15 +678,16 @@ def get_lookup_choices(self): @property def field(self): - if not hasattr(self, '_field'): + if not hasattr(self, "_field"): inner_field = super().field lookups = self.get_lookup_choices() self._field = self.outer_class( - inner_field, lookups, + inner_field, + lookups, label=self.label, empty_label=self.empty_label, - required=self.extra['required'], + required=self.extra["required"], ) return self._field @@ -692,29 +726,30 @@ class OrderingFilter(BaseCSVFilter, ChoiceFilter): for APIs. """ - descending_fmt = _('%s (descending)') + + descending_fmt = _("%s (descending)") def __init__(self, *args, **kwargs): """ ``fields`` may be either a mapping or an iterable. ``field_labels`` must be a map of field names to display labels """ - fields = kwargs.pop('fields', {}) + fields = kwargs.pop("fields", {}) fields = self.normalize_fields(fields) - field_labels = kwargs.pop('field_labels', {}) + field_labels = kwargs.pop("field_labels", {}) self.param_map = {v: k for k, v in fields.items()} - if 'choices' not in kwargs: - kwargs['choices'] = self.build_choices(fields, field_labels) + if "choices" not in kwargs: + kwargs["choices"] = self.build_choices(fields, field_labels) - kwargs.setdefault('label', _('Ordering')) - kwargs.setdefault('help_text', '') - kwargs.setdefault('null_label', None) + kwargs.setdefault("label", _("Ordering")) + kwargs.setdefault("help_text", "") + kwargs.setdefault("null_label", None) super().__init__(*args, **kwargs) def get_ordering_value(self, param): - descending = param.startswith('-') + descending = param.startswith("-") param = param[1:] if descending else param field_name = self.param_map.get(param, param) @@ -724,7 +759,11 @@ def filter(self, qs, value): if value in EMPTY_VALUES: return qs - ordering = [self.get_ordering_value(param) for param in value] + ordering = [ + self.get_ordering_value(param) + for param in value + if param not in EMPTY_VALUES + ] return qs.order_by(*ordering) @classmethod @@ -737,18 +776,19 @@ def normalize_fields(cls, fields): return OrderedDict(fields) # convert iterable of values => iterable of pairs (field name, param name) - assert is_iterable(fields), \ - "'fields' must be an iterable (e.g., a list, tuple, or mapping)." + assert is_iterable( + fields + ), "'fields' must be an iterable (e.g., a list, tuple, or mapping)." # fields is an iterable of field names - assert all(isinstance(field, str) or - is_iterable(field) and len(field) == 2 # may need to be wrapped in parens - for field in fields), \ - "'fields' must contain strings or (field name, param name) pairs." + assert all( + isinstance(field, str) + or is_iterable(field) + and len(field) == 2 # may need to be wrapped in parens + for field in fields + ), "'fields' must contain strings or (field name, param name) pairs." - return OrderedDict([ - (f, f) if isinstance(f, str) else f for f in fields - ]) + return OrderedDict([(f, f) if isinstance(f, str) else f for f in fields]) def build_choices(self, fields, labels): ascending = [ @@ -756,7 +796,7 @@ def build_choices(self, fields, labels): for field, param in fields.items() ] descending = [ - ('-%s' % param, labels.get('-%s' % param, self.descending_fmt % label)) + ("-%s" % param, labels.get("-%s" % param, self.descending_fmt % label)) for param, label in ascending ] @@ -769,6 +809,7 @@ class FilterMethod: This helper is used to override Filter.filter() when a 'method' argument is passed. It proxies the call to the actual method on the filter's parent. """ + def __init__(self, filter_instance): self.f = filter_instance @@ -790,15 +831,22 @@ def method(self): return instance.method # otherwise, method is the name of a method on the parent FilterSet. - assert hasattr(instance, 'parent'), \ - "Filter '%s' must have a parent FilterSet to find '.%s()'" % \ - (instance.field_name, instance.method) + assert hasattr( + instance, "parent" + ), "Filter '%s' must have a parent FilterSet to find '.%s()'" % ( + instance.field_name, + instance.method, + ) parent = instance.parent method = getattr(parent, instance.method, None) - assert callable(method), \ - "Expected parent FilterSet '%s.%s' to have a '.%s()' method." % \ - (parent.__class__.__module__, parent.__class__.__name__, instance.method) + assert callable( + method + ), "Expected parent FilterSet '%s.%s' to have a '.%s()' method." % ( + parent.__class__.__module__, + parent.__class__.__name__, + instance.method, + ) return method diff --git a/django_filters/filterset.py b/django_filters/filterset.py index e8982671a..1d90db704 100644 --- a/django_filters/filterset.py +++ b/django_filters/filterset.py @@ -4,11 +4,7 @@ from django import forms from django.db import models from django.db.models.constants import LOOKUP_SEP -from django.db.models.fields.related import ( - ManyToManyRel, - ManyToOneRel, - OneToOneRel -) +from django.db.models.fields.related import ManyToManyRel, ManyToOneRel, OneToOneRel from .conf import settings from .constants import ALL_FIELDS @@ -26,14 +22,9 @@ ModelMultipleChoiceFilter, NumberFilter, TimeFilter, - UUIDFilter -) -from .utils import ( - get_all_model_fields, - get_model_field, - resolve_field, - try_dbfield + UUIDFilter, ) +from .utils import get_all_model_fields, get_model_field, resolve_field, try_dbfield def remote_queryset(field): @@ -44,7 +35,7 @@ def remote_queryset(field): model = field.related_model # Reverse relationships do not have choice limits - if not hasattr(field, 'get_limit_choices_to'): + if not hasattr(field, "get_limit_choices_to"): return model._default_manager.all() limit_choices_to = field.get_limit_choices_to() @@ -53,31 +44,23 @@ def remote_queryset(field): class FilterSetOptions: def __init__(self, options=None): - self.model = getattr(options, 'model', None) - self.fields = getattr(options, 'fields', None) - self.exclude = getattr(options, 'exclude', None) + self.model = getattr(options, "model", None) + self.fields = getattr(options, "fields", None) + self.exclude = getattr(options, "exclude", None) - self.filter_overrides = getattr(options, 'filter_overrides', {}) + self.filter_overrides = getattr(options, "filter_overrides", {}) - self.form = getattr(options, 'form', forms.Form) + self.form = getattr(options, "form", forms.Form) class FilterSetMetaclass(type): def __new__(cls, name, bases, attrs): - attrs['declared_filters'] = cls.get_declared_filters(bases, attrs) + attrs["declared_filters"] = cls.get_declared_filters(bases, attrs) new_class = super().__new__(cls, name, bases, attrs) - new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None)) + new_class._meta = FilterSetOptions(getattr(new_class, "Meta", None)) new_class.base_filters = new_class.get_filters() - # TODO: remove assertion in 2.1 - assert not hasattr(new_class, 'filter_for_reverse_field'), ( - "`%(cls)s.filter_for_reverse_field` has been removed. " - "`%(cls)s.filter_for_field` now generates filters for reverse fields. " - "See: https://django-filter.readthedocs.io/en/master/guide/migration.html" - % {'cls': new_class.__name__} - ) - return new_class @classmethod @@ -90,7 +73,7 @@ def get_declared_filters(cls, bases, attrs): # Default the `filter.field_name` to the attribute name on the filterset for filter_name, f in filters: - if getattr(f, 'field_name', None) is None: + if getattr(f, "field_name", None) is None: f.field_name = filter_name filters.sort(key=lambda x: x[1].creation_counter) @@ -106,80 +89,80 @@ def visit(name): base_filters = [ (visit(name), f) - for base in bases if hasattr(base, 'declared_filters') - for name, f in base.declared_filters.items() if name not in known + for base in bases + if hasattr(base, "declared_filters") + for name, f in base.declared_filters.items() + if name not in known ] return OrderedDict(base_filters + filters) FILTER_FOR_DBFIELD_DEFAULTS = { - models.AutoField: {'filter_class': NumberFilter}, - models.CharField: {'filter_class': CharFilter}, - models.TextField: {'filter_class': CharFilter}, - models.BooleanField: {'filter_class': BooleanFilter}, - models.DateField: {'filter_class': DateFilter}, - models.DateTimeField: {'filter_class': DateTimeFilter}, - models.TimeField: {'filter_class': TimeFilter}, - models.DurationField: {'filter_class': DurationFilter}, - models.DecimalField: {'filter_class': NumberFilter}, - models.SmallIntegerField: {'filter_class': NumberFilter}, - models.IntegerField: {'filter_class': NumberFilter}, - models.PositiveIntegerField: {'filter_class': NumberFilter}, - models.PositiveSmallIntegerField: {'filter_class': NumberFilter}, - models.FloatField: {'filter_class': NumberFilter}, - models.NullBooleanField: {'filter_class': BooleanFilter}, - models.SlugField: {'filter_class': CharFilter}, - models.EmailField: {'filter_class': CharFilter}, - models.FilePathField: {'filter_class': CharFilter}, - models.URLField: {'filter_class': CharFilter}, - models.GenericIPAddressField: {'filter_class': CharFilter}, - models.CommaSeparatedIntegerField: {'filter_class': CharFilter}, - models.UUIDField: {'filter_class': UUIDFilter}, - + models.AutoField: {"filter_class": NumberFilter}, + models.CharField: {"filter_class": CharFilter}, + models.TextField: {"filter_class": CharFilter}, + models.BooleanField: {"filter_class": BooleanFilter}, + models.DateField: {"filter_class": DateFilter}, + models.DateTimeField: {"filter_class": DateTimeFilter}, + models.TimeField: {"filter_class": TimeFilter}, + models.DurationField: {"filter_class": DurationFilter}, + models.DecimalField: {"filter_class": NumberFilter}, + models.SmallIntegerField: {"filter_class": NumberFilter}, + models.IntegerField: {"filter_class": NumberFilter}, + models.PositiveIntegerField: {"filter_class": NumberFilter}, + models.PositiveSmallIntegerField: {"filter_class": NumberFilter}, + models.FloatField: {"filter_class": NumberFilter}, + models.NullBooleanField: {"filter_class": BooleanFilter}, + models.SlugField: {"filter_class": CharFilter}, + models.EmailField: {"filter_class": CharFilter}, + models.FilePathField: {"filter_class": CharFilter}, + models.URLField: {"filter_class": CharFilter}, + models.GenericIPAddressField: {"filter_class": CharFilter}, + models.CommaSeparatedIntegerField: {"filter_class": CharFilter}, + models.UUIDField: {"filter_class": UUIDFilter}, # Forward relationships models.OneToOneField: { - 'filter_class': ModelChoiceFilter, - 'extra': lambda f: { - 'queryset': remote_queryset(f), - 'to_field_name': f.remote_field.field_name, - 'null_label': settings.NULL_CHOICE_LABEL if f.null else None, - } + "filter_class": ModelChoiceFilter, + "extra": lambda f: { + "queryset": remote_queryset(f), + "to_field_name": f.remote_field.field_name, + "null_label": settings.NULL_CHOICE_LABEL if f.null else None, + }, }, models.ForeignKey: { - 'filter_class': ModelChoiceFilter, - 'extra': lambda f: { - 'queryset': remote_queryset(f), - 'to_field_name': f.remote_field.field_name, - 'null_label': settings.NULL_CHOICE_LABEL if f.null else None, - } + "filter_class": ModelChoiceFilter, + "extra": lambda f: { + "queryset": remote_queryset(f), + "to_field_name": f.remote_field.field_name, + "null_label": settings.NULL_CHOICE_LABEL if f.null else None, + }, }, models.ManyToManyField: { - 'filter_class': ModelMultipleChoiceFilter, - 'extra': lambda f: { - 'queryset': remote_queryset(f), - } + "filter_class": ModelMultipleChoiceFilter, + "extra": lambda f: { + "queryset": remote_queryset(f), + }, }, - # Reverse relationships OneToOneRel: { - 'filter_class': ModelChoiceFilter, - 'extra': lambda f: { - 'queryset': remote_queryset(f), - 'null_label': settings.NULL_CHOICE_LABEL if f.null else None, - } + "filter_class": ModelChoiceFilter, + "extra": lambda f: { + "queryset": remote_queryset(f), + "null_label": settings.NULL_CHOICE_LABEL if f.null else None, + }, }, ManyToOneRel: { - 'filter_class': ModelMultipleChoiceFilter, - 'extra': lambda f: { - 'queryset': remote_queryset(f), - } + "filter_class": ModelMultipleChoiceFilter, + "extra": lambda f: { + "queryset": remote_queryset(f), + }, }, ManyToManyRel: { - 'filter_class': ModelMultipleChoiceFilter, - 'extra': lambda f: { - 'queryset': remote_queryset(f), - } + "filter_class": ModelMultipleChoiceFilter, + "extra": lambda f: { + "queryset": remote_queryset(f), + }, }, } @@ -228,14 +211,18 @@ def filter_queryset(self, queryset): """ for name, value in self.form.cleaned_data.items(): queryset = self.filters[name].filter(queryset, value) - assert isinstance(queryset, models.QuerySet), \ - "Expected '%s.%s' to return a QuerySet, but got a %s instead." \ - % (type(self).__name__, name, type(queryset).__name__) + assert isinstance( + queryset, models.QuerySet + ), "Expected '%s.%s' to return a QuerySet, but got a %s instead." % ( + type(self).__name__, + name, + type(queryset).__name__, + ) return queryset @property def qs(self): - if not hasattr(self, '_qs'): + if not hasattr(self, "_qs"): qs = self.queryset.all() if self.is_bound: # ensure form validation before filtering @@ -251,16 +238,15 @@ def get_form_class(self): This method should be overridden if the form class needs to be customized relative to the filterset instance. """ - fields = OrderedDict([ - (name, filter_.field) - for name, filter_ in self.filters.items()]) + fields = OrderedDict( + [(name, filter_.field) for name, filter_ in self.filters.items()] + ) - return type(str('%sForm' % self.__class__.__name__), - (self._meta.form,), fields) + return type(str("%sForm" % self.__class__.__name__), (self._meta.form,), fields) @property def form(self): - if not hasattr(self, '_form'): + if not hasattr(self, "_form"): Form = self.get_form_class() if self.is_bound: self._form = Form(self.data, prefix=self.form_prefix) @@ -278,10 +264,11 @@ def get_fields(cls): fields = cls._meta.fields exclude = cls._meta.exclude - assert not (fields is None and exclude is None), \ - "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " \ - "has been deprecated since 0.15.0 and is now disallowed. Add an explicit " \ + assert not (fields is None and exclude is None), ( + "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " + "has been deprecated since 0.15.0 and is now disallowed. Add an explicit " "'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__ + ) # Setting exclude with no fields implies all other fields. if exclude is not None and fields is None: @@ -294,7 +281,9 @@ def get_fields(cls): # Remove excluded fields exclude = exclude or [] if not isinstance(fields, dict): - fields = [(f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude] + fields = [ + (f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude + ] else: fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude] @@ -312,7 +301,7 @@ def get_filter_name(cls, field_name, lookup_expr): # This also works with transformed exact lookups, such as 'date__exact' _default_expr = LOOKUP_SEP + settings.DEFAULT_LOOKUP_EXPR if filter_name.endswith(_default_expr): - filter_name = filter_name[:-len(_default_expr)] + filter_name = filter_name[: -len(_default_expr)] return filter_name @@ -348,7 +337,9 @@ def get_filters(cls): continue if field is not None: - filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr) + filters[filter_name] = cls.filter_for_field( + field, field_name, lookup_expr + ) # Allow Meta.fields to contain declared filters *only* when a list/tuple if isinstance(cls._meta.fields, (list, tuple)): @@ -357,7 +348,7 @@ def get_filters(cls): if undefined: raise TypeError( "'Meta.fields' must not contain non-model field names: %s" - % ', '.join(undefined) + % ", ".join(undefined) ) # Add in declared filters. This is necessary since we don't enforce adding @@ -372,8 +363,8 @@ def filter_for_field(cls, field, field_name, lookup_expr=None): field, lookup_type = resolve_field(field, lookup_expr) default = { - 'field_name': field_name, - 'lookup_expr': lookup_expr, + "field_name": field_name, + "lookup_expr": lookup_expr, } filter_class, params = cls.filter_for_lookup(field, lookup_type) @@ -382,7 +373,7 @@ def filter_for_field(cls, field, field_name, lookup_expr=None): assert filter_class is not None, ( "%s resolved field '%s' with '%s' lookup to an unrecognized field " "type %s. Try adding an override to 'Meta.filter_overrides'. See: " - "https://django-filter.readthedocs.io/en/master/ref/filterset.html" + "https://django-filter.readthedocs.io/en/main/ref/filterset.html" "#customise-filter-generation-with-filter-overrides" ) % (cls.__name__, field_name, lookup_expr, field.__class__.__name__) @@ -391,40 +382,44 @@ def filter_for_field(cls, field, field_name, lookup_expr=None): @classmethod def filter_for_lookup(cls, field, lookup_type): DEFAULTS = dict(cls.FILTER_DEFAULTS) - if hasattr(cls, '_meta'): + if hasattr(cls, "_meta"): DEFAULTS.update(cls._meta.filter_overrides) data = try_dbfield(DEFAULTS.get, field.__class__) or {} - filter_class = data.get('filter_class') - params = data.get('extra', lambda field: {})(field) + filter_class = data.get("filter_class") + params = data.get("extra", lambda field: {})(field) # if there is no filter class, exit early if not filter_class: return None, {} # perform lookup specific checks - if lookup_type == 'exact' and getattr(field, 'choices', None): - return ChoiceFilter, {'choices': field.choices} + if lookup_type == "exact" and getattr(field, "choices", None): + return ChoiceFilter, {"choices": field.choices} - if lookup_type == 'isnull': + if lookup_type == "isnull": data = try_dbfield(DEFAULTS.get, models.BooleanField) - filter_class = data.get('filter_class') - params = data.get('extra', lambda field: {})(field) + filter_class = data.get("filter_class") + params = data.get("extra", lambda field: {})(field) return filter_class, params - if lookup_type == 'in': + if lookup_type == "in": + class ConcreteInFilter(BaseInFilter, filter_class): pass + ConcreteInFilter.__name__ = cls._csv_filter_class_name( filter_class, lookup_type ) return ConcreteInFilter, params - if lookup_type == 'range': + if lookup_type == "range": + class ConcreteRangeFilter(BaseRangeFilter, filter_class): pass + ConcreteRangeFilter.__name__ = cls._csv_filter_class_name( filter_class, lookup_type ) @@ -449,14 +444,14 @@ def _csv_filter_class_name(cls, filter_class, lookup_type): """ # DateTimeFilter => DateTime type_name = filter_class.__name__ - if type_name.endswith('Filter'): + if type_name.endswith("Filter"): type_name = type_name[:-6] # in => In lookup_name = lookup_type.capitalize() # DateTimeInFilter - return str('%s%sFilter' % (type_name, lookup_name)) + return str("%s%sFilter" % (type_name, lookup_name)) class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass): @@ -464,7 +459,8 @@ class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass): def filterset_factory(model, fields=ALL_FIELDS): - meta = type(str('Meta'), (object,), {'model': model, 'fields': fields}) - filterset = type(str('%sFilterSet' % model._meta.object_name), - (FilterSet,), {'Meta': meta}) + meta = type(str("Meta"), (object,), {"model": model, "fields": fields}) + filterset = type( + str("%sFilterSet" % model._meta.object_name), (FilterSet,), {"Meta": meta} + ) return filterset diff --git a/django_filters/locale/ar/LC_MESSAGES/django.mo b/django_filters/locale/ar/LC_MESSAGES/django.mo new file mode 100644 index 000000000..b1b876f57 Binary files /dev/null and b/django_filters/locale/ar/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/ar/LC_MESSAGES/django.po b/django_filters/locale/ar/LC_MESSAGES/django.po new file mode 100644 index 000000000..9e5caee24 --- /dev/null +++ b/django_filters/locale/ar/LC_MESSAGES/django.po @@ -0,0 +1,192 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# FULL NAME , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2020-03-24 00:48+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Gtranslator 2.91.7\n" + +#: conf.py:16 +msgid "date" +msgstr "تاريخ" + +#: conf.py:17 +msgid "year" +msgstr "سنة" + +#: conf.py:18 +msgid "month" +msgstr "شهر" + +#: conf.py:19 +msgid "day" +msgstr "يوم" + +#: conf.py:20 +msgid "week day" +msgstr "يوم الأسبوع" + +#: conf.py:21 +msgid "hour" +msgstr "ساعة" + +#: conf.py:22 +msgid "minute" +msgstr "دقيقة" + +#: conf.py:23 +msgid "second" +msgstr "ثانية" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "يحتوي على" + +#: conf.py:29 +msgid "is in" +msgstr "في داخل" + +#: conf.py:30 +msgid "is greater than" +msgstr "أكبر من" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "أكبر من أو يساوي" + +#: conf.py:32 +msgid "is less than" +msgstr "أصغر من" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "أصغر من أو يساوي" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "يبدأ ب" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "ينتهي ب" + +#: conf.py:38 +msgid "is in range" +msgstr "في النطاق" + +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "يطابق التعبير العادي" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "بحث" + +#: conf.py:44 +msgid "is contained by" +msgstr "موجود في" + +#: conf.py:45 +msgid "overlaps" +msgstr "يتداخل" + +#: conf.py:46 +msgid "has key" +msgstr "لديه مفتاح" + +#: conf.py:47 +msgid "has keys" +msgstr "لديه مفاتيح" + +#: conf.py:48 +msgid "has any keys" +msgstr "لديه أي مفاتيح" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "حدد بحث" + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "إستعلام النطاق يتوقع قيمتين" + +#: filters.py:437 +msgid "Today" +msgstr "اليوم" + +#: filters.py:438 +msgid "Yesterday" +msgstr "أمس" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "الأيام السبعة الماضية" + +#: filters.py:440 +msgid "This month" +msgstr "هذا الشهر" + +#: filters.py:441 +msgid "This year" +msgstr "هذه السنة" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "يمكن فصل القيم المتعددة بفواصل." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (تنازلي)" + +#: filters.py:737 +msgid "Ordering" +msgstr "الترتيب" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "إرسال" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "مرشحات الحقل" + +#: utils.py:308 +msgid "exclude" +msgstr "استبعاد" + +#: widgets.py:58 +msgid "All" +msgstr "كل" + +#: widgets.py:162 +msgid "Unknown" +msgstr "مجهول" + +#: widgets.py:162 +msgid "Yes" +msgstr "نعم" + +#: widgets.py:162 +msgid "No" +msgstr "لا" diff --git a/django_filters/locale/be/LC_MESSAGES/django.mo b/django_filters/locale/be/LC_MESSAGES/django.mo index f78e804eb..595dad9ef 100644 Binary files a/django_filters/locale/be/LC_MESSAGES/django.mo and b/django_filters/locale/be/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/be/LC_MESSAGES/django.po b/django_filters/locale/be/LC_MESSAGES/django.po index 33c11c9a3..84ab34215 100644 --- a/django_filters/locale/be/LC_MESSAGES/django.po +++ b/django_filters/locale/be/LC_MESSAGES/django.po @@ -1,10 +1,9 @@ # -#: conf.py:27 conf.py:28 conf.py:41 msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-01-24 18:51+0500\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2016-09-29 11:47+0300\n" "Last-Translator: Eugena Mikhaylikova \n" "Language-Team: TextTempearture\n" @@ -12,148 +11,153 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" "X-Generator: Poedit 1.8.9\n" -#: conf.py:17 +#: conf.py:16 msgid "date" msgstr "дата" -#: conf.py:18 +#: conf.py:17 msgid "year" msgstr "год" -#: conf.py:19 +#: conf.py:18 msgid "month" msgstr "месяц" -#: conf.py:20 +#: conf.py:19 msgid "day" msgstr "дзень" -#: conf.py:21 +#: conf.py:20 msgid "week day" msgstr "дзень тыдня" -#: conf.py:22 +#: conf.py:21 msgid "hour" msgstr "гадзіну" -#: conf.py:23 +#: conf.py:22 msgid "minute" msgstr "хвіліна" -#: conf.py:24 +#: conf.py:23 msgid "second" msgstr "секунда" -#: conf.py:29 conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "змяшчае" -#: conf.py:31 +#: conf.py:29 msgid "is in" msgstr "у" -#: conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "больш чым" -#: conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "больш або роўна" -#: conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "менш чым" -#: conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "менш або роўна" -#: conf.py:36 conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "пачынаецца" -#: conf.py:38 conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "заканчваецца" -#: conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "у дыяпазоне" -#: conf.py:42 conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "адпавядае рэгулярнаму выразу" -#: conf.py:44 conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "пошук" -#: conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "змяшчаецца ў" -#: conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "перакрываецца" -#: conf.py:49 +#: conf.py:46 msgid "has key" msgstr "мае ключ" -#: conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "мае ключы" -#: conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "мае любыя ключы" -#: fields.py:178 +#: fields.py:94 +msgid "Select a lookup." +msgstr "" + +#: fields.py:198 msgid "Range query expects two values." msgstr "Запыт дыяпазону чакае два значэння." -#: filters.py:429 -msgid "Any date" -msgstr "Любая дата" - -#: filters.py:430 +#: filters.py:437 msgid "Today" msgstr "Сёння" -#: filters.py:435 +#: filters.py:438 +msgid "Yesterday" +msgstr "Учора" + +#: filters.py:439 msgid "Past 7 days" msgstr "Мінулыя 7 дзён" -#: filters.py:439 +#: filters.py:440 msgid "This month" msgstr "За гэты месяц" -#: filters.py:443 +#: filters.py:441 msgid "This year" msgstr "У гэтым годзе" -#: filters.py:446 -msgid "Yesterday" -msgstr "Учора" - -#: filters.py:512 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Некалькі значэнняў могуць быць падзеленыя коскамі." -#: filters.py:591 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (па змяншэнні)" -#: filters.py:607 +#: filters.py:737 msgid "Ordering" msgstr "Парадак" -#: rest_framework/filterset.py:30 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Адправіць" @@ -163,22 +167,25 @@ msgstr "Адправіць" msgid "Field filters" msgstr "Фільтры па палях" -#: utils.py:224 +#: utils.py:308 msgid "exclude" msgstr "выключаючы" -#: widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Усе" -#: widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Не было прапанавана" -#: widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Ды" -#: widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Няма" + +#~ msgid "Any date" +#~ msgstr "Любая дата" diff --git a/django_filters/locale/bg/LC_MESSAGES/django.mo b/django_filters/locale/bg/LC_MESSAGES/django.mo index db432d2af..122fe4df8 100644 Binary files a/django_filters/locale/bg/LC_MESSAGES/django.mo and b/django_filters/locale/bg/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/bg/LC_MESSAGES/django.po b/django_filters/locale/bg/LC_MESSAGES/django.po index 227f66c8f..476cc1228 100644 --- a/django_filters/locale/bg/LC_MESSAGES/django.po +++ b/django_filters/locale/bg/LC_MESSAGES/django.po @@ -3,12 +3,11 @@ # This file is distributed under the same license as the PACKAGE package. # Hristo Gatsinski , 2019. # -#: conf.py:27 conf.py:28 conf.py:41 msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-12-22 19:45+0200\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2019-12-21 19:36+0200\n" "Last-Translator: Hristo Gatsinski \n" "Language-Team: \n" @@ -16,107 +15,110 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.8.9\n" -#: conf.py:17 +#: conf.py:16 msgid "date" msgstr "дата" -#: conf.py:18 +#: conf.py:17 msgid "year" msgstr "година" -#: conf.py:19 +#: conf.py:18 msgid "month" msgstr "месец" -#: conf.py:20 +#: conf.py:19 msgid "day" msgstr "ден" -#: conf.py:21 +#: conf.py:20 msgid "week day" msgstr "ден от седмицата" -#: conf.py:22 +#: conf.py:21 msgid "hour" msgstr "час" -#: conf.py:23 +#: conf.py:22 msgid "minute" msgstr "минута" -#: conf.py:24 +#: conf.py:23 msgid "second" msgstr "секунда" -#: conf.py:29 conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "съдържа" -#: conf.py:31 +#: conf.py:29 msgid "is in" msgstr "в" -#: conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "е по-голям от" -#: conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "е по-голям или равен на" -#: conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "е по-малък от" -#: conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "е по-малък или равен на" -#: conf.py:36 conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "започва с" -#: conf.py:38 conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "завършва с" -#: conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "е в диапазона" -#: conf.py:42 conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "съвпада с регуларен израз" -#: conf.py:44 conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "търсене" -#: conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "се съдържа от" -#: conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "припокрива" -#: conf.py:49 +#: conf.py:46 msgid "has key" msgstr "има ключ" -#: conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "има ключове" -#: conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "има който и да е ключ" -#: fields.py:106 +#: fields.py:94 msgid "Select a lookup." msgstr "Изберете справка" @@ -124,40 +126,40 @@ msgstr "Изберете справка" msgid "Range query expects two values." msgstr "Търсенето по диапазон изисква две стойности" -#: filters.py:406 +#: filters.py:437 msgid "Today" msgstr "Днес" -#: filters.py:407 +#: filters.py:438 msgid "Yesterday" msgstr "Вчера" -#: filters.py:408 +#: filters.py:439 msgid "Past 7 days" msgstr "Последните 7 дни" -#: filters.py:409 +#: filters.py:440 msgid "This month" msgstr "Този месец" -#: filters.py:410 +#: filters.py:441 msgid "This year" msgstr "Тази година" -#: filters.py:508 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Множество стойности може да се разделят със запетая" -#: filters.py:681 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (намалавящ)" -#: filters.py:697 +#: filters.py:737 msgid "Ordering" msgstr "Подредба" -#: rest_framework/filterset.py:31 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Изпращане" @@ -167,22 +169,22 @@ msgstr "Изпращане" msgid "Field filters" msgstr "Филтри на полетата" -#: utils.py:298 +#: utils.py:308 msgid "exclude" msgstr "изключва" -#: widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Всичко" -#: widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Неизвестен" -#: widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Да" -#: widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Не" diff --git a/django_filters/locale/cs/LC_MESSAGES/django.mo b/django_filters/locale/cs/LC_MESSAGES/django.mo index dca2b3da3..54a8915c0 100644 Binary files a/django_filters/locale/cs/LC_MESSAGES/django.mo and b/django_filters/locale/cs/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/cs/LC_MESSAGES/django.po b/django_filters/locale/cs/LC_MESSAGES/django.po index bf02e0223..36d3d88d2 100644 --- a/django_filters/locale/cs/LC_MESSAGES/django.po +++ b/django_filters/locale/cs/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-01-24 11:03+0500\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2016-09-29 11:47+0300\n" "Last-Translator: Eugena Mikhaylikova \n" "Language-Team: TextTempearture\n" @@ -11,146 +11,152 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n " +"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" "X-Generator: Poedit 1.8.9\n" -#: conf.py:17 +#: conf.py:16 msgid "date" msgstr "datum" -#: conf.py:18 +#: conf.py:17 msgid "year" msgstr "rok" -#: conf.py:19 +#: conf.py:18 msgid "month" msgstr "měsíc" -#: conf.py:20 +#: conf.py:19 msgid "day" msgstr "den" -#: conf.py:21 +#: conf.py:20 msgid "week day" msgstr "den v týdnu" -#: conf.py:22 +#: conf.py:21 msgid "hour" msgstr "hodinu" -#: conf.py:23 +#: conf.py:22 msgid "minute" msgstr "minutu" -#: conf.py:24 +#: conf.py:23 msgid "second" msgstr "vteřina" -#: conf.py:29 conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "obsahuje" -#: conf.py:31 +#: conf.py:29 msgid "is in" msgstr "v" -#: conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "více než" -#: conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "větší nebo roven" -#: conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "méně než" -#: conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "menší nebo rovné" -#: conf.py:36 conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "začíná" -#: conf.py:38 conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "končí" -#: conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "v rozsahu" -#: conf.py:42 conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "odpovídá normálnímu výrazu" -#: conf.py:44 conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "vyhledávání" -#: conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "je obsažen v" -#: conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "překrývají" -#: conf.py:49 +#: conf.py:46 msgid "has key" msgstr "má klíč" -#: conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "má klíče" -#: conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "má nějaké klíče" -#: fields.py:178 +#: fields.py:94 +msgid "Select a lookup." +msgstr "" + +#: fields.py:198 msgid "Range query expects two values." msgstr "Rozsah dotazu očekává dvě hodnoty." -#: filters.py:429 -msgid "Any date" -msgstr "Jakékoliv datum" - -#: filters.py:430 +#: filters.py:437 msgid "Today" msgstr "Dnes" -#: filters.py:435 +#: filters.py:438 +msgid "Yesterday" +msgstr "Včera" + +#: filters.py:439 msgid "Past 7 days" msgstr "Posledních 7 dní" -#: filters.py:439 +#: filters.py:440 msgid "This month" msgstr "Tento měsíc" -#: filters.py:443 +#: filters.py:441 msgid "This year" msgstr "Tento rok" -#: filters.py:446 -msgid "Yesterday" -msgstr "Včera" - -#: filters.py:512 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Více hodnot lze oddělit čárkami." -#: filters.py:591 +#: filters.py:721 +#, python-format msgid "%s (descending)" msgstr "%s (sestupně)" -#: filters.py:607 +#: filters.py:737 msgid "Ordering" msgstr "Řád z" -#: rest_framework/filterset.py:30 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Odeslat" @@ -160,22 +166,25 @@ msgstr "Odeslat" msgid "Field filters" msgstr "Filtry na polích" -#: utils.py:224 +#: utils.py:308 msgid "exclude" msgstr "s výjimkou" -#: widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Všechno" -#: widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Není nastaveno" -#: widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Ano" -#: widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Ne" + +#~ msgid "Any date" +#~ msgstr "Jakékoliv datum" diff --git a/django_filters/locale/da/LC_MESSAGES/django.mo b/django_filters/locale/da/LC_MESSAGES/django.mo index 3942b2977..84f7e940e 100644 Binary files a/django_filters/locale/da/LC_MESSAGES/django.mo and b/django_filters/locale/da/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/da/LC_MESSAGES/django.po b/django_filters/locale/da/LC_MESSAGES/django.po index 795cd0ab8..de07d2895 100644 --- a/django_filters/locale/da/LC_MESSAGES/django.po +++ b/django_filters/locale/da/LC_MESSAGES/django.po @@ -1,153 +1,160 @@ msgid "" msgstr "" +"Project-Id-Version: django-filter\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2017-10-28\n" +"Last-Translator: Danni Randeris \n" +"Language-Team: Danni Randeris \n" +"Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.0.1\n" -"Project-Id-Version: django-filter\n" -"Language: da\n" -"Last-Translator: Danni Randeris \n" -"Language-Team: Danni Randeris \n" -"POT-Creation-Date: 2017-10-28\n" -"PO-Revision-Date: 2017-10-28\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: conf.py:17 +#: conf.py:16 msgid "date" msgstr "dato" -#: conf.py:18 +#: conf.py:17 msgid "year" msgstr "år" -#: conf.py:19 +#: conf.py:18 msgid "month" msgstr "måned" -#: conf.py:20 +#: conf.py:19 msgid "day" msgstr "dag" -#: conf.py:21 +#: conf.py:20 msgid "week day" msgstr "ugedag" -#: conf.py:22 +#: conf.py:21 msgid "hour" msgstr "time" -#: conf.py:23 +#: conf.py:22 msgid "minute" msgstr "minut" -#: conf.py:24 +#: conf.py:23 msgid "second" msgstr "sekund" -#: conf.py:29 conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "indeholder" -#: conf.py:31 +#: conf.py:29 msgid "is in" msgstr "er i" -#: conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "er større end" -#: conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "er større end eller lig med" -#: conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "er mindre end" -#: conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "er mindre end eller lig med" -#: conf.py:36 conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "starter med" -#: conf.py:38 conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "slutter med" -#: conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "er i intervallet" -#: conf.py:42 conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "matcher regex" -#: conf.py:44 conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "søg" -#: conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "er indeholdt af" -#: conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "overlapper" -#: conf.py:49 +#: conf.py:46 msgid "has key" msgstr "har string" -#: conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "har stringe" -#: conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "har hvilken som helst string" -#: fields.py:178 +#: fields.py:94 +msgid "Select a lookup." +msgstr "" + +#: fields.py:198 msgid "Range query expects two values." msgstr "Interval forespørgslen forventer to værdier." -#: filters.py:429 -msgid "Any date" -msgstr "Hvilken som helst dag" - -#: filters.py:430 +#: filters.py:437 msgid "Today" msgstr "I dag" -#: filters.py:435 +#: filters.py:438 +msgid "Yesterday" +msgstr "I går" + +#: filters.py:439 msgid "Past 7 days" msgstr "Sidste 7 dage" -#: filters.py:439 +#: filters.py:440 msgid "This month" msgstr "Denne måned" -#: filters.py:443 +#: filters.py:441 msgid "This year" msgstr "Dette år" -#: filters.py:446 -msgid "Yesterday" -msgstr "I går" - -#: filters.py:512 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Flere værdier kan adskilles via komma." -#: filters.py:591 +#: filters.py:721 +#, python-format msgid "%s (descending)" msgstr "%s (aftagende)" -#: filters.py:607 +#: filters.py:737 msgid "Ordering" msgstr "Sortering" -#: rest_framework/filterset.py:30 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 #, fuzzy msgid "Submit" @@ -159,22 +166,25 @@ msgstr "Indsend" msgid "Field filters" msgstr "Felt filtre" -#: utils.py:224 +#: utils.py:308 msgid "exclude" msgstr "udelad" -#: widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Alle" -#: widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Ukendt" -#: widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Ja" -#: widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Nej" + +#~ msgid "Any date" +#~ msgstr "Hvilken som helst dag" diff --git a/django_filters/locale/de/LC_MESSAGES/django.mo b/django_filters/locale/de/LC_MESSAGES/django.mo index 758be2f43..9a48a0a90 100644 Binary files a/django_filters/locale/de/LC_MESSAGES/django.mo and b/django_filters/locale/de/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/de/LC_MESSAGES/django.po b/django_filters/locale/de/LC_MESSAGES/django.po index 428a2ffc5..8a8c48f45 100644 --- a/django_filters/locale/de/LC_MESSAGES/django.po +++ b/django_filters/locale/de/LC_MESSAGES/django.po @@ -3,12 +3,11 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # -#: conf.py:27 conf.py:28 conf.py:41 msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-01-24 11:03+0500\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2013-08-10 12:29+0100\n" "Last-Translator: Florian Apolloner \n" "Language-Team: \n" @@ -19,144 +18,148 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.5.4\n" -#: conf.py:17 +#: conf.py:16 msgid "date" msgstr "Datum" -#: conf.py:18 +#: conf.py:17 msgid "year" msgstr "Jahr" -#: conf.py:19 +#: conf.py:18 msgid "month" msgstr "Monat" -#: conf.py:20 +#: conf.py:19 msgid "day" msgstr "Tag" -#: conf.py:21 +#: conf.py:20 msgid "week day" msgstr "Wochentag" -#: conf.py:22 +#: conf.py:21 msgid "hour" msgstr "Stunde" -#: conf.py:23 +#: conf.py:22 msgid "minute" msgstr "Minute" -#: conf.py:24 +#: conf.py:23 msgid "second" msgstr "Sekunde" -#: conf.py:29 conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "enthält" -#: conf.py:31 +#: conf.py:29 msgid "is in" msgstr "ist in" -#: conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "ist größer als" -#: conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "ist größer oder gleich" -#: conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "ist kleiner als" -#: conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "ist kleiner oder gleich" -#: conf.py:36 conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "beginnt mit" -#: conf.py:38 conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "endet mit" -#: conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "ist im Bereich" -#: conf.py:42 conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "passt auf Regex" -#: conf.py:44 conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "Suche" -#: conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "ist enthalten in" -#: conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "überlappen" -#: conf.py:49 +#: conf.py:46 msgid "has key" msgstr "hat Schlüssel" -#: conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "hat Schlüssel" -#: conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "hat beliebige Schlüssel" -#: fields.py:178 +#: fields.py:94 +msgid "Select a lookup." +msgstr "" + +#: fields.py:198 msgid "Range query expects two values." msgstr "Die Bereichsabfrage erwartet zwei Werte." -#: filters.py:429 -msgid "Any date" -msgstr "Alle Daten" - -#: filters.py:430 +#: filters.py:437 msgid "Today" msgstr "Heute" -#: filters.py:435 +#: filters.py:438 +msgid "Yesterday" +msgstr "Gestern" + +#: filters.py:439 msgid "Past 7 days" msgstr "Letzte 7 Tage" -#: filters.py:439 +#: filters.py:440 msgid "This month" msgstr "Diesen Monat" -#: filters.py:443 +#: filters.py:441 msgid "This year" msgstr "Dieses Jahr" -#: filters.py:446 -msgid "Yesterday" -msgstr "Gestern" - -#: filters.py:512 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Mehrere Werte können durch Kommas getrennt sein." -#: filters.py:591 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (absteigend)" -#: filters.py:607 +#: filters.py:737 msgid "Ordering" msgstr "Sortierung" -#: rest_framework/filterset.py:30 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Absenden" @@ -166,22 +169,25 @@ msgstr "Absenden" msgid "Field filters" msgstr "Feldfilter" -#: utils.py:224 +#: utils.py:308 msgid "exclude" msgstr "ausschließen" -#: widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Alle" -#: widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Unbekannte" -#: widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Ja" -#: widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Nein" + +#~ msgid "Any date" +#~ msgstr "Alle Daten" diff --git a/django_filters/locale/el/LC_MESSAGES/django.mo b/django_filters/locale/el/LC_MESSAGES/django.mo index fcec94d57..4e2258bdb 100644 Binary files a/django_filters/locale/el/LC_MESSAGES/django.mo and b/django_filters/locale/el/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/el/LC_MESSAGES/django.po b/django_filters/locale/el/LC_MESSAGES/django.po index 0018a2cf7..54a23537a 100644 --- a/django_filters/locale/el/LC_MESSAGES/django.po +++ b/django_filters/locale/el/LC_MESSAGES/django.po @@ -3,185 +3,191 @@ # This file is distributed under the same license as the PACKAGE package. # Serafeim Papastefanos , 2017. # -#: .\conf.py:27 .\conf.py:28 .\conf.py:41 msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-11-16 10:06+0200\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2017-11-16 10:04+0200\n" "Last-Translator: Serafeim Papastefanos \n" "Language-Team: \n" +"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.5\n" -#: .\conf.py:17 +#: conf.py:16 msgid "date" msgstr "ημερομηνία" -#: .\conf.py:18 +#: conf.py:17 msgid "year" msgstr "έτος" -#: .\conf.py:19 +#: conf.py:18 msgid "month" msgstr "μήνας" -#: .\conf.py:20 +#: conf.py:19 msgid "day" msgstr "ημέρα" -#: .\conf.py:21 +#: conf.py:20 msgid "week day" msgstr "ημέρα της εβδομάδας" -#: .\conf.py:22 +#: conf.py:21 msgid "hour" msgstr "ώρα" -#: .\conf.py:23 +#: conf.py:22 msgid "minute" msgstr "λεπτό" -#: .\conf.py:24 +#: conf.py:23 msgid "second" msgstr "δευτερόλεπτο" -#: .\conf.py:29 .\conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "περιέχει" -#: .\conf.py:31 +#: conf.py:29 msgid "is in" msgstr "είναι εντός των" -#: .\conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "είναι μεγαλύτερο από" -#: .\conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "είναι μεγαλύτερο ή ίσο του" -#: .\conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "είναι μικρότερο από" -#: .\conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "είναι μικρότερο ή ίσο του" -#: .\conf.py:36 .\conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "ξεκινά με" -#: .\conf.py:38 .\conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "τελειώνει με" -#: .\conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "είναι εντος του εύρους" -#: .\conf.py:42 .\conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "περιέχει regex" -#: .\conf.py:44 .\conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "αναζήτηση" -#: .\conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "περιέχεται σε" -#: .\conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "επικαλύπτεται" -#: .\conf.py:49 +#: conf.py:46 msgid "has key" msgstr "έχει το κλειδί" -#: .\conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "έχει τα κλειδιά" -#: .\conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "έχει οποιαδήποτε κλειδιά" -#: .\fields.py:178 +#: fields.py:94 +msgid "Select a lookup." +msgstr "" + +#: fields.py:198 msgid "Range query expects two values." msgstr "Το ερώτημα εύρους απαιτεί δύο τιμές," -#: .\filters.py:429 -msgid "Any date" -msgstr "Οποιαδήποτε ημερομηνία" - -#: .\filters.py:430 +#: filters.py:437 msgid "Today" msgstr "Σήμερα" -#: .\filters.py:435 +#: filters.py:438 +msgid "Yesterday" +msgstr "Χτες" + +#: filters.py:439 msgid "Past 7 days" msgstr "Τις προηγούμενες 7 ημέρες" -#: .\filters.py:439 +#: filters.py:440 msgid "This month" msgstr "Αυτό το μήνα" -#: .\filters.py:443 +#: filters.py:441 msgid "This year" msgstr "Αυτό το έτος" -#: .\filters.py:446 -msgid "Yesterday" -msgstr "Χτες" - -#: .\filters.py:512 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Οι πολλαπλές τιμές πρέπει να διαχωρίζονται με κόμμα." -#: .\filters.py:591 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (φθίνουσα" -#: .\filters.py:607 +#: filters.py:737 msgid "Ordering" msgstr "Ταξινόμηση" -#: .\rest_framework\filterset.py:30 -#: .\templates\django_filters\rest_framework\form.html:5 +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Υποβολή" -#: .\templates\django_filters\rest_framework\crispy_form.html:4 -#: .\templates\django_filters\rest_framework\form.html:2 +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Φίλτρα πεδίων" -#: .\utils.py:224 +#: utils.py:308 msgid "exclude" msgstr "απέκλεισε" -#: .\widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Όλα" -#: .\widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Άγνωστο" -#: .\widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Ναι" -#: .\widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Όχι" + +#~ msgid "Any date" +#~ msgstr "Οποιαδήποτε ημερομηνία" diff --git a/django_filters/locale/es/LC_MESSAGES/django.mo b/django_filters/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 000000000..3338ff7a7 Binary files /dev/null and b/django_filters/locale/es/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/es_ES/LC_MESSAGES/django.po b/django_filters/locale/es/LC_MESSAGES/django.po similarity index 65% rename from django_filters/locale/es_ES/LC_MESSAGES/django.po rename to django_filters/locale/es/LC_MESSAGES/django.po index 94ee09fcc..ef5b2ed8c 100644 --- a/django_filters/locale/es_ES/LC_MESSAGES/django.po +++ b/django_filters/locale/es/LC_MESSAGES/django.po @@ -2,184 +2,194 @@ # Copyright (C) 2013 # This file is distributed under the same license as the django_filter package. # Carlos Goce, 2017. +# Nicolás Stuardo, 2020 # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-01-26 20:32+0100\n" -"PO-Revision-Date: 2017-01-26 20:52+0100\n" -"Last-Translator: Carlos Goce\n" -"Language-Team: Spanish (España)\n" -"Language: es_ES\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2023-02-12 14:36+0000\n" +"Last-Translator: gallegonovato \n" +"Language-Team: Spanish \n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.8.11\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.16-dev\n" -#: conf.py:26 +#: conf.py:16 msgid "date" msgstr "fecha" -#: conf.py:27 +#: conf.py:17 msgid "year" msgstr "año" -#: conf.py:28 +#: conf.py:18 msgid "month" msgstr "mes" -#: conf.py:29 +#: conf.py:19 msgid "day" msgstr "día" -#: conf.py:30 +#: conf.py:20 msgid "week day" msgstr "día de la semana" -#: conf.py:31 +#: conf.py:21 msgid "hour" msgstr "hora" -#: conf.py:32 +#: conf.py:22 msgid "minute" msgstr "minuto" -#: conf.py:33 +#: conf.py:23 msgid "second" msgstr "segundo" -#: conf.py:38 conf.py:39 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "contiene" -#: conf.py:40 +#: conf.py:29 msgid "is in" msgstr "presente en" -#: conf.py:41 +#: conf.py:30 msgid "is greater than" msgstr "mayor que" -#: conf.py:42 +#: conf.py:31 msgid "is greater than or equal to" msgstr "mayor o igual que" -#: conf.py:43 +#: conf.py:32 msgid "is less than" msgstr "menor que" -#: conf.py:44 +#: conf.py:33 msgid "is less than or equal to" msgstr "menor o igual que" -#: conf.py:45 conf.py:46 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "comienza por" -#: conf.py:47 conf.py:48 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "termina por" -#: conf.py:49 +#: conf.py:38 msgid "is in range" msgstr "en el rango" -#: conf.py:51 conf.py:52 +#: conf.py:39 +msgid "is null" +msgstr "es nulo" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "coincide con la expresión regular" -#: conf.py:53 conf.py:61 +#: conf.py:42 conf.py:49 msgid "search" msgstr "buscar" -#: conf.py:56 +#: conf.py:44 msgid "is contained by" msgstr "contenido en" -#: conf.py:57 +#: conf.py:45 msgid "overlaps" msgstr "solapado" -#: conf.py:58 +#: conf.py:46 msgid "has key" msgstr "contiene la clave" -#: conf.py:59 +#: conf.py:47 msgid "has keys" msgstr "contiene las claves" -#: conf.py:60 +#: conf.py:48 msgid "has any keys" msgstr "contiene alguna de las claves" -#: fields.py:167 -msgid "Range query expects two values." -msgstr "Consultar un rango requiere dos valores" +#: fields.py:94 +msgid "Select a lookup." +msgstr "Seleccione un operador de consulta." -#: filters.py:443 -msgid "Any date" -msgstr "Cualquier fecha" +#: fields.py:198 +msgid "Range query expects two values." +msgstr "Consultar un rango requiere dos valores." -#: filters.py:444 +#: filters.py:437 msgid "Today" msgstr "Hoy" -#: filters.py:449 +#: filters.py:438 +msgid "Yesterday" +msgstr "Ayer" + +#: filters.py:439 msgid "Past 7 days" msgstr "Últimos 7 días" -#: filters.py:453 +#: filters.py:440 msgid "This month" msgstr "Este mes" -#: filters.py:457 +#: filters.py:441 msgid "This year" msgstr "Este año" -#: filters.py:460 -msgid "Yesterday" -msgstr "Ayer" - -#: filters.py:526 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Múltiples valores separados por comas." -#: filters.py:605 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (descendente)" -#: filters.py:621 +#: filters.py:737 msgid "Ordering" msgstr "Ordenado" -#: utils.py:220 +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Enviar" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Filtros de campo" + +#: utils.py:308 msgid "exclude" msgstr "excluye" -#: widgets.py:71 +#: widgets.py:58 msgid "All" msgstr "Todo" -#: widgets.py:119 +#: widgets.py:162 msgid "Unknown" msgstr "Desconocido" -#: widgets.py:120 +#: widgets.py:162 msgid "Yes" msgstr "Sí" -#: widgets.py:121 +#: widgets.py:162 msgid "No" msgstr "No" -#: rest_framework/filterset.py:31 -#: templates/django_filters/rest_framework/form.html:5 -msgid "Submit" -msgstr "Enviar" - -#: templates/django_filters/rest_framework/crispy_form.html:4 -#: templates/django_filters/rest_framework/form.html:2 -msgid "Field filters" -msgstr "Filtros de campo" +#~ msgid "Any date" +#~ msgstr "Cualquier fecha" diff --git a/django_filters/locale/es_AR/LC_MESSAGES/django.mo b/django_filters/locale/es_AR/LC_MESSAGES/django.mo index 0b88ee22d..7f4778af1 100644 Binary files a/django_filters/locale/es_AR/LC_MESSAGES/django.mo and b/django_filters/locale/es_AR/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/es_AR/LC_MESSAGES/django.po b/django_filters/locale/es_AR/LC_MESSAGES/django.po index 751dc8f08..f40b2af36 100644 --- a/django_filters/locale/es_AR/LC_MESSAGES/django.po +++ b/django_filters/locale/es_AR/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-07-05 19:24+0200\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2015-10-11 20:53-0300\n" "Last-Translator: Gonzalo Bustos\n" "Language-Team: Spanish (Argentina)\n" @@ -18,30 +18,184 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.10\n" -#: filters.py:51 -msgid "This is an exclusion filter" -msgstr "Este es un filtro de exclusión" - -#: filters.py:158 -msgid "Any date" +#: conf.py:16 +#, fuzzy +#| msgid "Any date" +msgid "date" msgstr "Cualquier fecha" -#: filters.py:159 +#: conf.py:17 +#, fuzzy +#| msgid "This year" +msgid "year" +msgstr "Este año" + +#: conf.py:18 +#, fuzzy +#| msgid "This month" +msgid "month" +msgstr "Este mes" + +#: conf.py:19 +#, fuzzy +#| msgid "Today" +msgid "day" +msgstr "Hoy" + +#: conf.py:20 +msgid "week day" +msgstr "" + +#: conf.py:21 +msgid "hour" +msgstr "" + +#: conf.py:22 +msgid "minute" +msgstr "" + +#: conf.py:23 +msgid "second" +msgstr "" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "" + +#: conf.py:29 +msgid "is in" +msgstr "" + +#: conf.py:30 +msgid "is greater than" +msgstr "" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "" + +#: conf.py:32 +msgid "is less than" +msgstr "" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "" + +#: conf.py:38 +msgid "is in range" +msgstr "" + +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "" + +#: conf.py:44 +msgid "is contained by" +msgstr "" + +#: conf.py:45 +msgid "overlaps" +msgstr "" + +#: conf.py:46 +msgid "has key" +msgstr "" + +#: conf.py:47 +msgid "has keys" +msgstr "" + +#: conf.py:48 +msgid "has any keys" +msgstr "" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "" + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "" + +#: filters.py:437 msgid "Today" msgstr "Hoy" -#: filters.py:164 +#: filters.py:438 +msgid "Yesterday" +msgstr "" + +#: filters.py:439 msgid "Past 7 days" msgstr "Últimos 7 días" -#: filters.py:168 +#: filters.py:440 msgid "This month" msgstr "Este mes" -#: filters.py:172 +#: filters.py:441 msgid "This year" msgstr "Este año" -#: widgets.py:63 +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "" + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "" + +#: filters.py:737 +msgid "Ordering" +msgstr "" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "" + +#: utils.py:308 +msgid "exclude" +msgstr "" + +#: widgets.py:58 msgid "All" msgstr "Todos" + +#: widgets.py:162 +msgid "Unknown" +msgstr "" + +#: widgets.py:162 +msgid "Yes" +msgstr "" + +#: widgets.py:162 +msgid "No" +msgstr "" + +#~ msgid "This is an exclusion filter" +#~ msgstr "Este es un filtro de exclusión" diff --git a/django_filters/locale/fa/LC_MESSAGES/django.mo b/django_filters/locale/fa/LC_MESSAGES/django.mo new file mode 100644 index 000000000..18cb47248 Binary files /dev/null and b/django_filters/locale/fa/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/fa/LC_MESSAGES/django.po b/django_filters/locale/fa/LC_MESSAGES/django.po new file mode 100644 index 000000000..4092d30bf --- /dev/null +++ b/django_filters/locale/fa/LC_MESSAGES/django.po @@ -0,0 +1,190 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: conf.py:16 +msgid "date" +msgstr "تاریخ" + +#: conf.py:17 +msgid "year" +msgstr "سال" + +#: conf.py:18 +msgid "month" +msgstr "ماه" + +#: conf.py:19 +msgid "day" +msgstr "روز" + +#: conf.py:20 +msgid "week day" +msgstr "روز هفته" + +#: conf.py:21 +msgid "hour" +msgstr "ساعت" + +#: conf.py:22 +msgid "minute" +msgstr "دقیقه" + +#: conf.py:23 +msgid "second" +msgstr "ثانیه" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "شامل" + +#: conf.py:29 +msgid "is in" +msgstr "هست در" + +#: conf.py:30 +msgid "is greater than" +msgstr "بزرگتر است از" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "بزرگتر یا مساوی است" + +#: conf.py:32 +msgid "is less than" +msgstr "کوچکتر است از" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "کوچکتر یا مساوی است" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "شروع می شود با" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "به پایان می رسد با" + +#: conf.py:38 +msgid "is in range" +msgstr "در محدوده" + +#: conf.py:39 +msgid "is null" +msgstr "خالی است" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "با ریجکس منطبق است" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "جستجو" + +#: conf.py:44 +msgid "is contained by" +msgstr "وجود دارد در" + +#: conf.py:45 +msgid "overlaps" +msgstr "تداخل دارد" + +#: conf.py:46 +msgid "has key" +msgstr "حاوی کلید است" + +#: conf.py:47 +msgid "has keys" +msgstr "حاوی کلیدها است" + +#: conf.py:48 +msgid "has any keys" +msgstr "حاوی هر کلیدی است" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "یک لوک آپ را انتخاب کنید." + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "محدوده کوئری دو مقدار را انتظار دارد." + +#: filters.py:437 +msgid "Today" +msgstr "امروز" + +#: filters.py:438 +msgid "Yesterday" +msgstr "دیروز" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "۷ روز گذشته" + +#: filters.py:440 +msgid "This month" +msgstr "این ماه" + +#: filters.py:441 +msgid "This year" +msgstr "امسال" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "ممکن است چندین مقدار با کاما از هم جدا شوند." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (نزولی)" + +#: filters.py:737 +msgid "Ordering" +msgstr "مرتب سازی" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "ارسال" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "فیلترهای فیلد" + +#: utils.py:308 +msgid "exclude" +msgstr "به غیر از" + +#: widgets.py:58 +msgid "All" +msgstr "همه" + +#: widgets.py:162 +msgid "Unknown" +msgstr "ناشناس" + +#: widgets.py:162 +msgid "Yes" +msgstr "بله" + +#: widgets.py:162 +msgid "No" +msgstr "خیر" diff --git a/django_filters/locale/fi/LC_MESSAGES/django.po b/django_filters/locale/fi/LC_MESSAGES/django.po new file mode 100644 index 000000000..9309fe4b1 --- /dev/null +++ b/django_filters/locale/fi/LC_MESSAGES/django.po @@ -0,0 +1,191 @@ +# Django Filter translation. +# Copyright (C) 2013 +# This file is distributed under the same license as the django_filter package. +# Carlos Goce, 2017. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-02-10 17:45+0200\n" +"PO-Revision-Date: 2023-02-12 14:36+0000\n" +"Last-Translator: Janne Tervo \n" +"Language-Team: Finnish \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.16-dev\n" + +#: conf.py:16 +msgid "date" +msgstr "päivämäärä" + +#: conf.py:17 +msgid "year" +msgstr "vuosi" + +#: conf.py:18 +msgid "month" +msgstr "kuukausi" + +#: conf.py:19 +msgid "day" +msgstr "päivä" + +#: conf.py:20 +msgid "week day" +msgstr "viikonpäivä" + +#: conf.py:21 +msgid "hour" +msgstr "tunti" + +#: conf.py:22 +msgid "minute" +msgstr "minuutti" + +#: conf.py:23 +msgid "second" +msgstr "sekunti" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "sisältää" + +#: conf.py:29 +msgid "is in" +msgstr "löytyy" + +#: conf.py:30 +msgid "is greater than" +msgstr "suurempi kuin" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "suurempi tai yhtäsuuri kuin" + +#: conf.py:32 +msgid "is less than" +msgstr "pienempi kuin" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "pienempi tai yhtäsuuri kuin" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "alkaa" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "päättyy" + +#: conf.py:38 +msgid "is in range" +msgstr "on välillä" + +#: conf.py:39 +msgid "is null" +msgstr "on null" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "täsmää säännölliseen lausekkeeseen" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "hae" + +#: conf.py:44 +msgid "is contained by" +msgstr "sisältyy kokonaan" + +#: conf.py:45 +msgid "overlaps" +msgstr "on päällekkäinen" + +#: conf.py:46 +msgid "has key" +msgstr "sisältää avaimen" + +#: conf.py:47 +msgid "has keys" +msgstr "sisältää avaimet" + +#: conf.py:48 +msgid "has any keys" +msgstr "sisältää minkä tahansa avaimista" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "Hakuehto vaaditaan." + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "Välin hakuun tarvitaan kaksi arvoa." + +#: filters.py:437 +msgid "Today" +msgstr "Tänään" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Eilen" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Edelliset 7 päivää" + +#: filters.py:440 +msgid "This month" +msgstr "Tässä kuussa" + +#: filters.py:441 +msgid "This year" +msgstr "Tänä vuonna" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Voit syöttää useita arvoja pilkulla erotettuna." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (laskeva)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Järjestä" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Lähetä" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Kenttävalinnat" + +#: utils.py:312 +msgid "exclude" +msgstr "poissulje" + +#: widgets.py:58 +msgid "All" +msgstr "Kaikki" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Tuntematon" + +#: widgets.py:162 +msgid "Yes" +msgstr "Kyllä" + +#: widgets.py:162 +msgid "No" +msgstr "Ei" diff --git a/django_filters/locale/fr/LC_MESSAGES/django.mo b/django_filters/locale/fr/LC_MESSAGES/django.mo index feafd02b4..2ae9948f3 100644 Binary files a/django_filters/locale/fr/LC_MESSAGES/django.mo and b/django_filters/locale/fr/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/fr/LC_MESSAGES/django.po b/django_filters/locale/fr/LC_MESSAGES/django.po index 8926219dd..2d4da2442 100644 --- a/django_filters/locale/fr/LC_MESSAGES/django.po +++ b/django_filters/locale/fr/LC_MESSAGES/django.po @@ -3,45 +3,192 @@ # This file is distributed under the same license as the django_filter package. # Axel Haustant , 2013. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-07-05 19:24+0200\n" -"PO-Revision-Date: 2013-07-05 19:24+0200\n" -"Last-Translator: Axel Haustant \n" -"Language-Team: LANGUAGE \n" -"Language: French\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2024-01-18 14:00+0000\n" +"Last-Translator: Nils Van Zuijlen \n" +"Language-Team: French \n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.4-dev\n" -#: filters.py:51 -msgid "This is an exclusion filter" -msgstr "Ceci est un filtre d'exclusion" +#: conf.py:16 +msgid "date" +msgstr "date" -#: filters.py:158 -msgid "Any date" -msgstr "Toutes les dates" +#: conf.py:17 +msgid "year" +msgstr "année" -#: filters.py:159 +#: conf.py:18 +msgid "month" +msgstr "mois" + +#: conf.py:19 +msgid "day" +msgstr "jour" + +#: conf.py:20 +msgid "week day" +msgstr "jour de la semaine" + +#: conf.py:21 +msgid "hour" +msgstr "heure" + +#: conf.py:22 +msgid "minute" +msgstr "minute" + +#: conf.py:23 +msgid "second" +msgstr "seconde" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "contient" + +#: conf.py:29 +msgid "is in" +msgstr "est inclus dans" + +#: conf.py:30 +msgid "is greater than" +msgstr "supérieur à" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "supérieur ou égal à" + +#: conf.py:32 +msgid "is less than" +msgstr "inférieur à" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "inférieur ou égale à" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "commence par" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "se termine par" + +#: conf.py:38 +msgid "is in range" +msgstr "entre" + +#: conf.py:39 +msgid "is null" +msgstr "est nul" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "correspond à l'expression régulière" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "recherche" + +#: conf.py:44 +msgid "is contained by" +msgstr "est contenu dans" + +#: conf.py:45 +msgid "overlaps" +msgstr "chevauche" + +#: conf.py:46 +msgid "has key" +msgstr "contient la clé" + +#: conf.py:47 +msgid "has keys" +msgstr "contient les clés" + +#: conf.py:48 +msgid "has any keys" +msgstr "a une des clés" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "Sélectionner un opérateur." + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "La fourchette doit avoir 2 valeurs." + +#: filters.py:437 msgid "Today" msgstr "Aujourd'hui" -#: filters.py:164 +#: filters.py:438 +msgid "Yesterday" +msgstr "Hier" + +#: filters.py:439 msgid "Past 7 days" msgstr "7 derniers jours" -#: filters.py:168 +#: filters.py:440 msgid "This month" msgstr "Ce mois-ci" -#: filters.py:172 +#: filters.py:441 msgid "This year" msgstr "Cette année" -#: widgets.py:63 +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Les valeurs multiples doivent être séparées par des virgules." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (décroissant)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Tri" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Envoyer" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Filtres de champ" + +#: utils.py:308 +msgid "exclude" +msgstr "Exclut" + +#: widgets.py:58 msgid "All" msgstr "Tous" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Inconnu" + +#: widgets.py:162 +msgid "Yes" +msgstr "Oui" + +#: widgets.py:162 +msgid "No" +msgstr "Non" + +#~ msgid "This is an exclusion filter" +#~ msgstr "Ceci est un filtre d'exclusion" diff --git a/django_filters/locale/es_ES/LC_MESSAGES/django.mo b/django_filters/locale/it/LC_MESSAGES/django.mo similarity index 54% rename from django_filters/locale/es_ES/LC_MESSAGES/django.mo rename to django_filters/locale/it/LC_MESSAGES/django.mo index dc5c4c239..f53c4eaa1 100644 Binary files a/django_filters/locale/es_ES/LC_MESSAGES/django.mo and b/django_filters/locale/it/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/it/LC_MESSAGES/django.po b/django_filters/locale/it/LC_MESSAGES/django.po new file mode 100644 index 000000000..7f8da301d --- /dev/null +++ b/django_filters/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,194 @@ +# Django Filter translation. +# Copyright (C) 2013 +# This file is distributed under the same license as the django_filter package. +# Carlos Goce, 2017. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2023-06-11 16:51+0000\n" +"Last-Translator: Daniele Tricoli \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.18-dev\n" + +#: conf.py:16 +msgid "date" +msgstr "data" + +#: conf.py:17 +msgid "year" +msgstr "anno" + +#: conf.py:18 +msgid "month" +msgstr "mese" + +#: conf.py:19 +msgid "day" +msgstr "giorno" + +#: conf.py:20 +msgid "week day" +msgstr "giorno della settimana" + +#: conf.py:21 +msgid "hour" +msgstr "ora" + +#: conf.py:22 +msgid "minute" +msgstr "minuto" + +#: conf.py:23 +msgid "second" +msgstr "secondo" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "contiene" + +#: conf.py:29 +msgid "is in" +msgstr "presente in" + +#: conf.py:30 +msgid "is greater than" +msgstr "maggiore di" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "maggiore o uguale di" + +#: conf.py:32 +msgid "is less than" +msgstr "minore di" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "minore o uguale di" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "comincia per" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "termina per" + +#: conf.py:38 +msgid "is in range" +msgstr "nell'intervallo" + +#: conf.py:39 +msgid "is null" +msgstr "nullo" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "coincide con la espressione regolare" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "cerca" + +#: conf.py:44 +msgid "is contained by" +msgstr "contenuto in" + +#: conf.py:45 +msgid "overlaps" +msgstr "sovrapposto" + +#: conf.py:46 +msgid "has key" +msgstr "contiene la chiave" + +#: conf.py:47 +msgid "has keys" +msgstr "contiene le chiavi" + +#: conf.py:48 +msgid "has any keys" +msgstr "contiene qualsiasi chiave" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "" + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "La query di intervallo richiede due valori." + +#: filters.py:437 +msgid "Today" +msgstr "Oggi" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Ieri" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Ultimi 7 giorni" + +#: filters.py:440 +msgid "This month" +msgstr "Questo mese" + +#: filters.py:441 +msgid "This year" +msgstr "Questo anno" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Più valori separati da virgole." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (decrescente)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Ordinamento" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Invia" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Filtri del campo" + +#: utils.py:308 +msgid "exclude" +msgstr "escludi" + +#: widgets.py:58 +msgid "All" +msgstr "Tutti" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Sconosciuto" + +#: widgets.py:162 +msgid "Yes" +msgstr "Sì" + +#: widgets.py:162 +msgid "No" +msgstr "No" + +#~ msgid "Any date" +#~ msgstr "Qualsiasi data" diff --git a/django_filters/locale/nl/LC_MESSAGES/django.mo b/django_filters/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 000000000..dcfae01a5 Binary files /dev/null and b/django_filters/locale/nl/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/nl/LC_MESSAGES/django.po b/django_filters/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 000000000..e640c19f3 --- /dev/null +++ b/django_filters/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,189 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-21 12:25+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Storm Heg \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: conf.py:16 +msgid "date" +msgstr "datum" + +#: conf.py:17 +msgid "year" +msgstr "jaar" + +#: conf.py:18 +msgid "month" +msgstr "maand" + +#: conf.py:19 +msgid "day" +msgstr "dag" + +#: conf.py:20 +msgid "week day" +msgstr "weekdag" + +#: conf.py:21 +msgid "hour" +msgstr "uur" + +#: conf.py:22 +msgid "minute" +msgstr "minuur" + +#: conf.py:23 +msgid "second" +msgstr "seconde" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "bevat" + +#: conf.py:29 +msgid "is in" +msgstr "zit in" + +#: conf.py:30 +msgid "is greater than" +msgstr "is groter dan" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "is groter dan of gelijk aan" + +#: conf.py:32 +msgid "is less than" +msgstr "is minder dan" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "is minder dan of gelijk aan" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "begint met" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "eindigt met" + +#: conf.py:38 +msgid "is in range" +msgstr "zit in bereik" + +#: conf.py:39 +msgid "is null" +msgstr "is null" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "matcht regex" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "zoek" + +#: conf.py:44 +msgid "is contained by" +msgstr "wordt bevat door" + +#: conf.py:45 +msgid "overlaps" +msgstr "overlapt" + +#: conf.py:46 +msgid "has key" +msgstr "heeft key" + +#: conf.py:47 +msgid "has keys" +msgstr "heeft keys" + +#: conf.py:48 +msgid "has any keys" +msgstr "heeft keys" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "Selecteer een lookup." + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "Bereik query verwacht twee waarden." + +#: filters.py:437 +msgid "Today" +msgstr "Vandaag" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Gisteren" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Afgelopen 7 dagen" + +#: filters.py:440 +msgid "This month" +msgstr "Deze maand" + +#: filters.py:441 +msgid "This year" +msgstr "Dit jaar" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Meerdere waarden kunnen gescheiden worden door komma's." + +#: filters.py:721 tests/test_filters.py:1670 +#, python-format +msgid "%s (descending)" +msgstr "%s (aflopend)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Volgorde" + +#: rest_framework/filterset.py:33 +#: templates/rest_framework/form.html:5 +msgid "Submit" +msgstr "Indienen" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Veld filters" + +#: utils.py:323 +msgid "exclude" +msgstr "uitsluiten" + +#: widgets.py:58 +msgid "All" +msgstr "Alles" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Onbekend" + +#: widgets.py:162 +msgid "Yes" +msgstr "Ja" + +#: widgets.py:162 +msgid "No" +msgstr "Nee" diff --git a/django_filters/locale/pl/LC_MESSAGES/django.mo b/django_filters/locale/pl/LC_MESSAGES/django.mo index e6412e705..fdbf09d48 100644 Binary files a/django_filters/locale/pl/LC_MESSAGES/django.mo and b/django_filters/locale/pl/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/pl/LC_MESSAGES/django.po b/django_filters/locale/pl/LC_MESSAGES/django.po index 9ef204d05..3417fe8ed 100644 --- a/django_filters/locale/pl/LC_MESSAGES/django.po +++ b/django_filters/locale/pl/LC_MESSAGES/django.po @@ -3,199 +3,197 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # -#: conf.py:35 conf.py:36 conf.py:49 msgid "" msgstr "" "Project-Id-Version: django_filters 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-01 17:21+0000\n" -"PO-Revision-Date: 2015-07-25 01:27+0100\n" -"Last-Translator: Adam Dobrawy \n" -"Language-Team: Adam Dobrawy \n" -"Language: pl_PL\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2023-04-10 20:47+0000\n" +"Last-Translator: Quadric \n" +"Language-Team: Polish \n" +"Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " -"|| n%100>=20) ? 1 : 2);\n" -"X-Generator: Poedit 1.5.4\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" +"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" +"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Generator: Weblate 4.17-dev\n" -#: conf.py:25 -#, fuzzy -#| msgid "Any date" +#: conf.py:16 msgid "date" -msgstr "Dowolna data" +msgstr "data" -#: conf.py:26 -#, fuzzy -#| msgid "This year" +#: conf.py:17 msgid "year" -msgstr "Ten rok" +msgstr "rok" -#: conf.py:27 -#, fuzzy -#| msgid "This month" +#: conf.py:18 msgid "month" -msgstr "Ten miesiąc" +msgstr "miesiąc" -#: conf.py:28 -#, fuzzy -#| msgid "Today" +#: conf.py:19 msgid "day" -msgstr "Dziś" +msgstr "dzień" -#: conf.py:29 +#: conf.py:20 msgid "week day" msgstr "dzień tygodnia" -#: conf.py:30 +#: conf.py:21 msgid "hour" msgstr "godzina" -#: conf.py:31 +#: conf.py:22 msgid "minute" msgstr "minuta" -#: conf.py:32 +#: conf.py:23 msgid "second" -msgstr "" +msgstr "sekunda" -#: conf.py:37 conf.py:38 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "zawiera" -#: conf.py:39 +#: conf.py:29 msgid "is in" msgstr "zawiera się w" -#: conf.py:40 +#: conf.py:30 msgid "is greater than" msgstr "powyżej" -#: conf.py:41 +#: conf.py:31 msgid "is greater than or equal to" msgstr "powyżej lub równe" -#: conf.py:42 +#: conf.py:32 msgid "is less than" msgstr "poniżej" -#: conf.py:43 +#: conf.py:33 msgid "is less than or equal to" msgstr "poniżej lub równe" -#: conf.py:44 conf.py:45 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "zaczyna się od" -#: conf.py:46 conf.py:47 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "kończy się na" -#: conf.py:48 +#: conf.py:38 msgid "is in range" msgstr "zawiera się w zakresie" -#: conf.py:50 conf.py:51 +#: conf.py:39 +msgid "is null" +msgstr "jest wartością null" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "pasuje do wyrażenia regularnego" -#: conf.py:52 conf.py:60 +#: conf.py:42 conf.py:49 msgid "search" msgstr "szukaj" -#: conf.py:55 +#: conf.py:44 msgid "is contained by" msgstr "zawiera się w" -#: conf.py:56 +#: conf.py:45 msgid "overlaps" -msgstr "" +msgstr "nakłada się" -#: conf.py:57 +#: conf.py:46 msgid "has key" -msgstr "" +msgstr "posiada klucz" -#: conf.py:58 +#: conf.py:47 msgid "has keys" -msgstr "" +msgstr "posiada klucze" -#: conf.py:59 +#: conf.py:48 msgid "has any keys" -msgstr "" +msgstr "posiada jakiekolwiek klucze" -#: fields.py:172 -msgid "Range query expects two values." -msgstr "" +#: fields.py:94 +msgid "Select a lookup." +msgstr "Wybierz wyszukiwanie." -#: filters.py:452 -msgid "Any date" -msgstr "Dowolna data" +#: fields.py:198 +msgid "Range query expects two values." +msgstr "Zapytanie o zakres oczekuje dwóch wartości." -#: filters.py:453 +#: filters.py:437 msgid "Today" msgstr "Dziś" -#: filters.py:458 +#: filters.py:438 +msgid "Yesterday" +msgstr "Wczoraj" + +#: filters.py:439 msgid "Past 7 days" msgstr "Ostatnie 7 dni" -#: filters.py:462 +#: filters.py:440 msgid "This month" msgstr "Ten miesiąc" -#: filters.py:466 +#: filters.py:441 msgid "This year" msgstr "Ten rok" -#: filters.py:469 -msgid "Yesterday" -msgstr "Wczoraj" - -#: filters.py:535 +#: filters.py:543 msgid "Multiple values may be separated by commas." -msgstr "Wiele wartości można rozdzielić przecinkami" +msgstr "Wiele wartości można rozdzielić przecinkami." -#: filters.py:614 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (malejąco)" -#: filters.py:630 +#: filters.py:737 msgid "Ordering" msgstr "Sortowanie" -#: rest_framework/filterset.py:34 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" -msgstr "" +msgstr "Wyślij" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 -#, fuzzy -#| msgid "Filter" msgid "Field filters" -msgstr "Filter" +msgstr "Filtry pola" -#: utils.py:225 +#: utils.py:308 msgid "exclude" -msgstr "" +msgstr "wyklucz" -#: widgets.py:66 +#: widgets.py:58 msgid "All" msgstr "Wszystko" -#: widgets.py:173 +#: widgets.py:162 msgid "Unknown" -msgstr "" +msgstr "Nieznane" -#: widgets.py:174 +#: widgets.py:162 msgid "Yes" msgstr "Tak" -#: widgets.py:175 +#: widgets.py:162 msgid "No" msgstr "Nie" +#~ msgid "Any date" +#~ msgstr "Dowolna data" + #~ msgid "This is an exclusion filter" #~ msgstr "Jest to filtr wykluczający" diff --git a/django_filters/locale/pt_BR/LC_MESSAGES/django.mo b/django_filters/locale/pt_BR/LC_MESSAGES/django.mo index 0aa399199..e9cc1a888 100644 Binary files a/django_filters/locale/pt_BR/LC_MESSAGES/django.mo and b/django_filters/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/pt_BR/LC_MESSAGES/django.po b/django_filters/locale/pt_BR/LC_MESSAGES/django.po index 6f8eb0ff2..73fca97a5 100644 --- a/django_filters/locale/pt_BR/LC_MESSAGES/django.po +++ b/django_filters/locale/pt_BR/LC_MESSAGES/django.po @@ -7,180 +7,188 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-12-11 22:04+0100\n" -"PO-Revision-Date: 2017-12-11 22:07-0200\n" -"Last-Translator: Anderson Scouto da Silva\n" -"Language-Team: \n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2023-06-30 13:51+0000\n" +"Last-Translator: Diogo Silva \n" +"Language-Team: Portuguese (Brazil) \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.8.13\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.0-dev\n" -#: conf.py:26 +#: conf.py:16 msgid "date" msgstr "data" -#: conf.py:27 +#: conf.py:17 msgid "year" msgstr "ano" -#: conf.py:28 +#: conf.py:18 msgid "month" msgstr "mês" -#: conf.py:29 +#: conf.py:19 msgid "day" msgstr "dia" -#: conf.py:30 +#: conf.py:20 msgid "week day" msgstr "dia da semana" -#: conf.py:31 +#: conf.py:21 msgid "hour" msgstr "hora" -#: conf.py:32 +#: conf.py:22 msgid "minute" msgstr "minuto" -#: conf.py:33 +#: conf.py:23 msgid "second" msgstr "segundo" -#: conf.py:38 conf.py:39 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "contém" -#: conf.py:40 +#: conf.py:29 msgid "is in" msgstr "presente em" -#: conf.py:41 +#: conf.py:30 msgid "is greater than" msgstr "é maior que" -#: conf.py:42 +#: conf.py:31 msgid "is greater than or equal to" msgstr "é maior ou igual que" -#: conf.py:43 +#: conf.py:32 msgid "is less than" msgstr "é menor que" -#: conf.py:44 +#: conf.py:33 msgid "is less than or equal to" msgstr "é menor ou igual que" -#: conf.py:45 conf.py:46 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "começa com" -#: conf.py:47 conf.py:48 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "termina com" -#: conf.py:49 +#: conf.py:38 msgid "is in range" msgstr "está no range" -#: conf.py:51 conf.py:52 +#: conf.py:39 +msgid "is null" +msgstr "é nulo" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "coincide com a expressão regular" -#: conf.py:53 conf.py:61 +#: conf.py:42 conf.py:49 msgid "search" msgstr "buscar" -#: conf.py:56 +#: conf.py:44 msgid "is contained by" msgstr "está contido por" -#: conf.py:57 +#: conf.py:45 msgid "overlaps" msgstr "sobrepõe" -#: conf.py:58 +#: conf.py:46 msgid "has key" msgstr "contém a chave" -#: conf.py:59 +#: conf.py:47 msgid "has keys" msgstr "contém as chaves" -#: conf.py:60 +#: conf.py:48 msgid "has any keys" msgstr "contém uma das chaves" -#: fields.py:167 +#: fields.py:94 +msgid "Select a lookup." +msgstr "Selecione uma pesquisa." + +#: fields.py:198 msgid "Range query expects two values." msgstr "Consulta por range requer dois valores." -#: filters.py:443 -msgid "Any date" -msgstr "Qualquer data" - -#: filters.py:444 +#: filters.py:437 msgid "Today" msgstr "Hoje" -#: filters.py:449 +#: filters.py:438 +msgid "Yesterday" +msgstr "Ontem" + +#: filters.py:439 msgid "Past 7 days" msgstr "Últimos 7 dias" -#: filters.py:453 +#: filters.py:440 msgid "This month" msgstr "Este mês" -#: filters.py:457 +#: filters.py:441 msgid "This year" msgstr "Este ano" -#: filters.py:460 -msgid "Yesterday" -msgstr "Ontem" - -#: filters.py:526 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Valores múltiplos podem ser separados por vírgulas." -#: filters.py:605 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (decrescente)" -#: filters.py:621 +#: filters.py:737 msgid "Ordering" msgstr "Ordenado" -#: utils.py:220 +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Enviar" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Filtros de campo" + +#: utils.py:308 msgid "exclude" msgstr "excluir" -#: widgets.py:71 +#: widgets.py:58 msgid "All" msgstr "Tudo" -#: widgets.py:119 +#: widgets.py:162 msgid "Unknown" msgstr "Desconhecido" -#: widgets.py:120 +#: widgets.py:162 msgid "Yes" msgstr "Sim" -#: widgets.py:121 +#: widgets.py:162 msgid "No" msgstr "Não" -#: rest_framework/filterset.py:31 -#: templates/django_filters/rest_framework/form.html:5 -msgid "Submit" -msgstr "Enviar" - -#: templates/django_filters/rest_framework/crispy_form.html:4 -#: templates/django_filters/rest_framework/form.html:2 -msgid "Field filters" -msgstr "Filtros de campo" +#~ msgid "Any date" +#~ msgstr "Qualquer data" diff --git a/django_filters/locale/ro/LC_MESSAGES/django.po b/django_filters/locale/ro/LC_MESSAGES/django.po new file mode 100644 index 000000000..a622b232d --- /dev/null +++ b/django_filters/locale/ro/LC_MESSAGES/django.po @@ -0,0 +1,192 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-02-10 14:47+0000\n" +"PO-Revision-Date: 2023-02-10 16:28+0000\n" +"Last-Translator: Dan Braghis \n" +"Language-Team: Romanian \n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" +"X-Generator: Weblate 4.16-dev\n" + +#: conf.py:16 +msgid "date" +msgstr "dată" + +#: conf.py:17 +msgid "year" +msgstr "an" + +#: conf.py:18 +msgid "month" +msgstr "lună" + +#: conf.py:19 +msgid "day" +msgstr "zi" + +#: conf.py:20 +msgid "week day" +msgstr "zi a săptămânii" + +#: conf.py:21 +msgid "hour" +msgstr "oră" + +#: conf.py:22 +msgid "minute" +msgstr "minută" + +#: conf.py:23 +msgid "second" +msgstr "secundă" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "conține" + +#: conf.py:29 +msgid "is in" +msgstr "este în" + +#: conf.py:30 +msgid "is greater than" +msgstr "este mai mare decât" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "este mai mare sau egal cu" + +#: conf.py:32 +msgid "is less than" +msgstr "este mai mic decât" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "este mai mic sau egal cu" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "începe cu" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "se termină cu" + +#: conf.py:38 +msgid "is in range" +msgstr "este în intervalul" + +#: conf.py:39 +msgid "is null" +msgstr "este nul" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "se potrivește cu expresia regex" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "căutare" + +#: conf.py:44 +msgid "is contained by" +msgstr "cuprins de" + +#: conf.py:45 +msgid "overlaps" +msgstr "se suprapune" + +#: conf.py:46 +msgid "has key" +msgstr "are cheia" + +#: conf.py:47 +msgid "has keys" +msgstr "are cheile" + +#: conf.py:48 +msgid "has any keys" +msgstr "are orice cheie" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "Selectați o căutare." + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "Interogarea de interval așteaptă două valori." + +#: filters.py:437 +msgid "Today" +msgstr "Astăzi" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Ieri" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Ultimele 7 zile" + +#: filters.py:440 +msgid "This month" +msgstr "Luna aceasta" + +#: filters.py:441 +msgid "This year" +msgstr "Anul acesta" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Valorile multiple pot fi separate prin virgule." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (descescător)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Rânduire" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Trimite" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Filtre de câmp" + +#: utils.py:312 +msgid "exclude" +msgstr "exclude" + +#: widgets.py:58 +msgid "All" +msgstr "Toate" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Necunoscut" + +#: widgets.py:162 +msgid "Yes" +msgstr "Da" + +#: widgets.py:162 +msgid "No" +msgstr "Nu" diff --git a/django_filters/locale/ru/LC_MESSAGES/django.mo b/django_filters/locale/ru/LC_MESSAGES/django.mo index 0f3ede444..a4ff1ce33 100644 Binary files a/django_filters/locale/ru/LC_MESSAGES/django.mo and b/django_filters/locale/ru/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/ru/LC_MESSAGES/django.po b/django_filters/locale/ru/LC_MESSAGES/django.po index 3270aeb01..5a6159539 100644 --- a/django_filters/locale/ru/LC_MESSAGES/django.po +++ b/django_filters/locale/ru/LC_MESSAGES/django.po @@ -3,12 +3,11 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # -#: conf.py:27 conf.py:28 conf.py:41 msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-01-24 11:03+0500\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2016-09-29 11:47+0300\n" "Last-Translator: Mikhail Mitrofanov \n" "Language-Team: \n" @@ -16,148 +15,153 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" "X-Generator: Poedit 1.8.9\n" -#: conf.py:17 +#: conf.py:16 msgid "date" msgstr "дата" -#: conf.py:18 +#: conf.py:17 msgid "year" msgstr "год" -#: conf.py:19 +#: conf.py:18 msgid "month" msgstr "месяц" -#: conf.py:20 +#: conf.py:19 msgid "day" msgstr "день" -#: conf.py:21 +#: conf.py:20 msgid "week day" msgstr "день недели" -#: conf.py:22 +#: conf.py:21 msgid "hour" msgstr "час" -#: conf.py:23 +#: conf.py:22 msgid "minute" msgstr "минута" -#: conf.py:24 +#: conf.py:23 msgid "second" msgstr "секунда" -#: conf.py:29 conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "содержит" -#: conf.py:31 +#: conf.py:29 msgid "is in" msgstr "в" -#: conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "больше чем" -#: conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "больше или равно" -#: conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "меньше чем" -#: conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "меньше или равно" -#: conf.py:36 conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "начинается" -#: conf.py:38 conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "заканчивается" -#: conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "в диапазоне" -#: conf.py:42 conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "соответствует регулярному выражению" -#: conf.py:44 conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "поиск" -#: conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "содержится в" -#: conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "перекрывается" -#: conf.py:49 +#: conf.py:46 msgid "has key" msgstr "имеет ключ" -#: conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "имеет ключи" -#: conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "имеет любые ключи" -#: fields.py:178 +#: fields.py:94 +msgid "Select a lookup." +msgstr "" + +#: fields.py:198 msgid "Range query expects two values." msgstr "Запрос диапазона ожидает два значения." -#: filters.py:429 -msgid "Any date" -msgstr "Любая дата" - -#: filters.py:430 +#: filters.py:437 msgid "Today" msgstr "Сегодня" -#: filters.py:435 +#: filters.py:438 +msgid "Yesterday" +msgstr "Вчера" + +#: filters.py:439 msgid "Past 7 days" msgstr "Прошедшие 7 дней" -#: filters.py:439 +#: filters.py:440 msgid "This month" msgstr "За этот месяц" -#: filters.py:443 +#: filters.py:441 msgid "This year" msgstr "В этом году" -#: filters.py:446 -msgid "Yesterday" -msgstr "Вчера" - -#: filters.py:512 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Несколько значений могут быть разделены запятыми." -#: filters.py:591 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (по убыванию)" -#: filters.py:607 +#: filters.py:737 msgid "Ordering" msgstr "Порядок" -#: rest_framework/filterset.py:30 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Отправить" @@ -167,22 +171,25 @@ msgstr "Отправить" msgid "Field filters" msgstr "Фильтры по полям" -#: utils.py:224 +#: utils.py:308 msgid "exclude" msgstr "исключая" -#: widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Все" -#: widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Не задано" -#: widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Да" -#: widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Нет" + +#~ msgid "Any date" +#~ msgstr "Любая дата" diff --git a/django_filters/locale/sk/LC_MESSAGES/django.mo b/django_filters/locale/sk/LC_MESSAGES/django.mo index c4a0f8d13..d9c67d890 100644 Binary files a/django_filters/locale/sk/LC_MESSAGES/django.mo and b/django_filters/locale/sk/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/sk/LC_MESSAGES/django.po b/django_filters/locale/sk/LC_MESSAGES/django.po index c249f8b74..385b3d3e4 100644 --- a/django_filters/locale/sk/LC_MESSAGES/django.po +++ b/django_filters/locale/sk/LC_MESSAGES/django.po @@ -3,160 +3,166 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-03-25 19:11+0200\n" -"PO-Revision-Date: 2018-03-25 19:18+0058\n" -"Last-Translator: b'Erik Telepovsky '\n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2023-07-21 19:07+0000\n" +"Last-Translator: Milan Šalka \n" +"Language-Team: Slovak \n" +"Language: sk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n " +">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n" +"X-Generator: Weblate 5.0-dev\n" "X-Translated-Using: django-rosetta 0.8.1\n" -#: conf.py:17 +#: conf.py:16 msgid "date" msgstr "dátum" -#: conf.py:18 +#: conf.py:17 msgid "year" msgstr "rok" -#: conf.py:19 +#: conf.py:18 msgid "month" msgstr "mesiac" -#: conf.py:20 +#: conf.py:19 msgid "day" msgstr "deň" -#: conf.py:21 +#: conf.py:20 msgid "week day" msgstr "deň týždňa" -#: conf.py:22 +#: conf.py:21 msgid "hour" msgstr "hodina" -#: conf.py:23 +#: conf.py:22 msgid "minute" msgstr "minúta" -#: conf.py:24 +#: conf.py:23 msgid "second" msgstr "sekunda" -#: conf.py:29 conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "obsahuje" -#: conf.py:31 +#: conf.py:29 msgid "is in" msgstr "je v" -#: conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "je vačší než" -#: conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "je vačší alebo rovný ako" -#: conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "je menší než" -#: conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "je menší alebo rovný ako" -#: conf.py:36 conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "začína s" -#: conf.py:38 conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "končí s" -#: conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "je v rozsahu" -#: conf.py:42 conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "je nulová" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "spĺňa regex" -#: conf.py:44 conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "hľadať" -#: conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "je obsiahnutý" -#: conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "presahuje" -#: conf.py:49 +#: conf.py:46 msgid "has key" msgstr "má kľúč" -#: conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "má kľúče" -#: conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "má akékoľvek kľúče" -#: fields.py:178 +#: fields.py:94 +msgid "Select a lookup." +msgstr "Vyberte vyhľadávanie." + +#: fields.py:198 msgid "Range query expects two values." msgstr "Rozsah očakáva dve hodnoty." -#: filters.py:429 -msgid "Any date" -msgstr "Akýkoľvek dátum" - -#: filters.py:430 +#: filters.py:437 msgid "Today" msgstr "Dnes" -#: filters.py:435 +#: filters.py:438 +msgid "Yesterday" +msgstr "Včera" + +#: filters.py:439 msgid "Past 7 days" msgstr "Posledných 7 dní" -#: filters.py:439 +#: filters.py:440 msgid "This month" msgstr "Tento mesiac" -#: filters.py:443 +#: filters.py:441 msgid "This year" msgstr "Tento rok" -#: filters.py:446 -msgid "Yesterday" -msgstr "Včera" - -#: filters.py:512 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Viacero hodnôt môže byť oddelených čiarkami." -#: filters.py:591 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (klesajúco)" -#: filters.py:607 +#: filters.py:737 msgid "Ordering" msgstr "Zoradenie" -#: rest_framework/filterset.py:30 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Potvrdiť" @@ -166,22 +172,25 @@ msgstr "Potvrdiť" msgid "Field filters" msgstr "Filtre poľa" -#: utils.py:224 +#: utils.py:308 msgid "exclude" msgstr "neobsahuje" -#: widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Všetky" -#: widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Neznáme" -#: widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Áno" -#: widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Nie" + +#~ msgid "Any date" +#~ msgstr "Akýkoľvek dátum" diff --git a/django_filters/locale/uk/LC_MESSAGES/django.mo b/django_filters/locale/uk/LC_MESSAGES/django.mo index 428c0bd61..e16148d14 100644 Binary files a/django_filters/locale/uk/LC_MESSAGES/django.mo and b/django_filters/locale/uk/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/uk/LC_MESSAGES/django.po b/django_filters/locale/uk/LC_MESSAGES/django.po index 28485cf29..ead6f2d7e 100644 --- a/django_filters/locale/uk/LC_MESSAGES/django.po +++ b/django_filters/locale/uk/LC_MESSAGES/django.po @@ -3,154 +3,163 @@ msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-01-24 11:03+0500\n" -"PO-Revision-Date: 2016-09-29 11:47+0300\n" -"Last-Translator: Eugena Mikhaylikova \n" -"Language-Team: TextTempearture\n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2024-01-01 15:10+0000\n" +"Last-Translator: Сергій \n" +"Language-Team: Ukrainian \n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -"X-Generator: Poedit 1.8.9\n" +"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 " +"? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > " +"14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % " +"100 >=11 && n % 100 <=14 )) ? 2: 3);\n" +"X-Generator: Weblate 5.4-dev\n" -#: conf.py:17 +#: conf.py:16 msgid "date" msgstr "дата" -#: conf.py:18 +#: conf.py:17 msgid "year" msgstr "рік" -#: conf.py:19 +#: conf.py:18 msgid "month" msgstr "місяць" -#: conf.py:20 +#: conf.py:19 msgid "day" msgstr "день" -#: conf.py:21 +#: conf.py:20 msgid "week day" msgstr "день тижня" -#: conf.py:22 +#: conf.py:21 msgid "hour" msgstr "година" -#: conf.py:23 +#: conf.py:22 msgid "minute" msgstr "хвилина" -#: conf.py:24 +#: conf.py:23 msgid "second" msgstr "секунда" -#: conf.py:29 conf.py:30 +#: conf.py:27 conf.py:28 msgid "contains" msgstr "містить" -#: conf.py:31 +#: conf.py:29 msgid "is in" msgstr "в" -#: conf.py:32 +#: conf.py:30 msgid "is greater than" msgstr "більше ніж" -#: conf.py:33 +#: conf.py:31 msgid "is greater than or equal to" msgstr "більше або дорівнює" -#: conf.py:34 +#: conf.py:32 msgid "is less than" msgstr "менше ніж" -#: conf.py:35 +#: conf.py:33 msgid "is less than or equal to" msgstr "менше або дорівнює" -#: conf.py:36 conf.py:37 +#: conf.py:34 conf.py:35 msgid "starts with" msgstr "починається" -#: conf.py:38 conf.py:39 +#: conf.py:36 conf.py:37 msgid "ends with" msgstr "закінчується" -#: conf.py:40 +#: conf.py:38 msgid "is in range" msgstr "в діапазоні" -#: conf.py:42 conf.py:43 +#: conf.py:39 +msgid "is null" +msgstr "є порожнім" + +#: conf.py:40 conf.py:41 msgid "matches regex" msgstr "відповідає регулярному виразу" -#: conf.py:44 conf.py:52 +#: conf.py:42 conf.py:49 msgid "search" msgstr "пошук" -#: conf.py:47 +#: conf.py:44 msgid "is contained by" msgstr "міститься в" -#: conf.py:48 +#: conf.py:45 msgid "overlaps" msgstr "перекривається" -#: conf.py:49 +#: conf.py:46 msgid "has key" msgstr "має ключ" -#: conf.py:50 +#: conf.py:47 msgid "has keys" msgstr "має ключі" -#: conf.py:51 +#: conf.py:48 msgid "has any keys" msgstr "має будь-які ключі" -#: fields.py:178 +#: fields.py:94 +msgid "Select a lookup." +msgstr "Оберіть оператор запиту." + +#: fields.py:198 msgid "Range query expects two values." msgstr "Запит діапазону очікує два значення." -#: filters.py:429 -msgid "Any date" -msgstr "Будь-яка дата" - -#: filters.py:430 +#: filters.py:437 msgid "Today" msgstr "Сьогодні" -#: filters.py:435 +#: filters.py:438 +msgid "Yesterday" +msgstr "Вчора" + +#: filters.py:439 msgid "Past 7 days" msgstr "Минулі 7 днів" -#: filters.py:439 +#: filters.py:440 msgid "This month" msgstr "За цей місяць" -#: filters.py:443 +#: filters.py:441 msgid "This year" msgstr "В цьому році" -#: filters.py:446 -msgid "Yesterday" -msgstr "Вчора" - -#: filters.py:512 +#: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Кілька значень можуть бути розділені комами." -#: filters.py:591 +#: filters.py:721 +#, python-format msgid "%s (descending)" msgstr "%s (по спадаючій)" -#: filters.py:607 +#: filters.py:737 msgid "Ordering" msgstr "Порядок" -#: rest_framework/filterset.py:30 +#: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Відправити" @@ -160,22 +169,25 @@ msgstr "Відправити" msgid "Field filters" msgstr "Фільтри по полях" -#: utils.py:224 +#: utils.py:308 msgid "exclude" msgstr "виключаючи" -#: widgets.py:57 +#: widgets.py:58 msgid "All" msgstr "Усе" -#: widgets.py:159 +#: widgets.py:162 msgid "Unknown" msgstr "Не задано" -#: widgets.py:160 +#: widgets.py:162 msgid "Yes" msgstr "Так" -#: widgets.py:161 +#: widgets.py:162 msgid "No" msgstr "Немає" + +#~ msgid "Any date" +#~ msgstr "Будь-яка дата" diff --git a/django_filters/locale/zh_CN/LC_MESSAGES/django.mo b/django_filters/locale/zh_CN/LC_MESSAGES/django.mo new file mode 100644 index 000000000..7047a5e13 Binary files /dev/null and b/django_filters/locale/zh_CN/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/zh_CN/LC_MESSAGES/django.po b/django_filters/locale/zh_CN/LC_MESSAGES/django.po index de067b9eb..9fb5ce9d7 100644 --- a/django_filters/locale/zh_CN/LC_MESSAGES/django.po +++ b/django_filters/locale/zh_CN/LC_MESSAGES/django.po @@ -3,62 +3,192 @@ # This file is distributed under the same license as the PACKAGE package. # Kane Blueriver , 2016. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-30 17:39+0800\n" -"PO-Revision-Date: 2016-01-30 17:50+0800\n" -"Last-Translator: Kane Blueriver \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"POT-Creation-Date: 2023-02-10 11:07+0000\n" +"PO-Revision-Date: 2023-05-07 03:57+0000\n" +"Last-Translator: Lattefang <370358679@qq.com>\n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.18-dev\n" -#: filters.py:62 -msgid "This is an exclusion filter" -msgstr "未启用该过滤器" +#: conf.py:16 +msgid "date" +msgstr "日期" -#: filters.py:62 -msgid "Filter" -msgstr "过滤器" +#: conf.py:17 +msgid "year" +msgstr "年" -#: filters.py:264 -msgid "Any date" -msgstr "任何时刻" +#: conf.py:18 +msgid "month" +msgstr "月" -#: filters.py:265 +#: conf.py:19 +msgid "day" +msgstr "日" + +#: conf.py:20 +msgid "week day" +msgstr "工作日" + +#: conf.py:21 +msgid "hour" +msgstr "小时" + +#: conf.py:22 +msgid "minute" +msgstr "分钟" + +#: conf.py:23 +msgid "second" +msgstr "秒" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "包含" + +#: conf.py:29 +msgid "is in" +msgstr "在" + +#: conf.py:30 +msgid "is greater than" +msgstr "大于" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "大于等于" + +#: conf.py:32 +msgid "is less than" +msgstr "小于" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "小于等于" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "以……开始" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "以……结尾" + +#: conf.py:38 +msgid "is in range" +msgstr "在范围内" + +#: conf.py:39 +msgid "is null" +msgstr "为空" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "匹配正则表达式" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "搜索" + +#: conf.py:44 +msgid "is contained by" +msgstr "包含在" + +#: conf.py:45 +msgid "overlaps" +msgstr "重叠" + +#: conf.py:46 +msgid "has key" +msgstr "单值" + +#: conf.py:47 +msgid "has keys" +msgstr "多值" + +#: conf.py:48 +msgid "has any keys" +msgstr "任何值" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "选择查找。" + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "范围查询需要两个值。" + +#: filters.py:437 msgid "Today" msgstr "今日" -#: filters.py:270 +#: filters.py:438 +msgid "Yesterday" +msgstr "昨日" + +#: filters.py:439 msgid "Past 7 days" msgstr "过去 7 日" -#: filters.py:274 +#: filters.py:440 msgid "This month" msgstr "本月" -#: filters.py:278 +#: filters.py:441 msgid "This year" msgstr "今年" -#: filters.py:281 -msgid "Yesterday" -msgstr "昨日" +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "多个值可以用逗号分隔。" -#: filterset.py:398 filterset.py:409 +#: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s(降序)" -#: filterset.py:411 +#: filters.py:737 msgid "Ordering" msgstr "排序" -#: widgets.py:60 +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "提交" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "字段过滤器" + +#: utils.py:308 +msgid "exclude" +msgstr "排除" + +#: widgets.py:58 msgid "All" msgstr "全部" + +#: widgets.py:162 +msgid "Unknown" +msgstr "未知" + +#: widgets.py:162 +msgid "Yes" +msgstr "是" + +#: widgets.py:162 +msgid "No" +msgstr "否" + +#~ msgid "This is an exclusion filter" +#~ msgstr "未启用该过滤器" diff --git a/django_filters/rest_framework/backends.py b/django_filters/rest_framework/backends.py index 835a67f9a..bb2ae3529 100644 --- a/django_filters/rest_framework/backends.py +++ b/django_filters/rest_framework/backends.py @@ -1,31 +1,20 @@ import warnings from django.template import loader -from django.utils.deprecation import RenameMethodsBase from .. import compat, utils from . import filters, filterset -# TODO: remove metaclass in 2.1 -class RenameAttributes(utils.RenameAttributesBase, RenameMethodsBase): - renamed_attributes = ( - ('default_filter_set', 'filterset_base', utils.MigrationNotice), - ) - renamed_methods = ( - ('get_filter_class', 'get_filterset_class', utils.MigrationNotice), - ) - - -class DjangoFilterBackend(metaclass=RenameAttributes): +class DjangoFilterBackend: filterset_base = filterset.FilterSet raise_exception = True @property def template(self): if compat.is_crispy(): - return 'django_filters/rest_framework/crispy_form.html' - return 'django_filters/rest_framework/form.html' + return "django_filters/rest_framework/crispy_form.html" + return "django_filters/rest_framework/form.html" def get_filterset(self, request, queryset, view): filterset_class = self.get_filterset_class(view, queryset) @@ -39,36 +28,25 @@ def get_filterset_class(self, view, queryset=None): """ Return the `FilterSet` class used to filter the queryset. """ - filterset_class = getattr(view, 'filterset_class', None) - filterset_fields = getattr(view, 'filterset_fields', None) - - # TODO: remove assertion in 2.1 - if filterset_class is None and hasattr(view, 'filter_class'): - utils.deprecate( - "`%s.filter_class` attribute should be renamed `filterset_class`." - % view.__class__.__name__) - filterset_class = getattr(view, 'filter_class', None) - - # TODO: remove assertion in 2.1 - if filterset_fields is None and hasattr(view, 'filter_fields'): - utils.deprecate( - "`%s.filter_fields` attribute should be renamed `filterset_fields`." - % view.__class__.__name__) - filterset_fields = getattr(view, 'filter_fields', None) + filterset_class = getattr(view, "filterset_class", None) + filterset_fields = getattr(view, "filterset_fields", None) if filterset_class: filterset_model = filterset_class._meta.model # FilterSets do not need to specify a Meta class if filterset_model and queryset is not None: - assert issubclass(queryset.model, filterset_model), \ - 'FilterSet model %s does not match queryset model %s' % \ - (filterset_model, queryset.model) + assert issubclass( + queryset.model, filterset_model + ), "FilterSet model %s does not match queryset model %s" % ( + filterset_model, + queryset.model, + ) return filterset_class if filterset_fields and queryset is not None: - MetaBase = getattr(self.filterset_base, 'Meta', object) + MetaBase = getattr(self.filterset_base, "Meta", object) class AutoFilterSet(self.filterset_base): class Meta(MetaBase): @@ -81,9 +59,9 @@ class Meta(MetaBase): def get_filterset_kwargs(self, request, queryset, view): return { - 'data': request.query_params, - 'queryset': queryset, - 'request': request, + "data": request.query_params, + "queryset": queryset, + "request": request, } def filter_queryset(self, request, queryset, view): @@ -101,7 +79,7 @@ def to_html(self, request, queryset, view): return None template = loader.get_template(self.template) - context = {'filter': filterset} + context = {"filter": filterset} return template.render(context, request) def get_coreschema_field(self, field): @@ -109,16 +87,23 @@ def get_coreschema_field(self, field): field_cls = compat.coreschema.Number else: field_cls = compat.coreschema.String - return field_cls( - description=str(field.extra.get('help_text', '')) - ) + return field_cls(description=str(field.extra.get("help_text", ""))) def get_schema_fields(self, view): # This is not compatible with widgets where the query param differs from the # filter's attribute name. Notably, this includes `MultiWidget`, where query # params will be of the format `_0`, `_1`, etc... - assert compat.coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' - assert compat.coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' + from django_filters import RemovedInDjangoFilter25Warning + warnings.warn( + "Built-in schema generation is deprecated. Use drf-spectacular.", + category=RemovedInDjangoFilter25Warning, + ) + assert ( + compat.coreapi is not None + ), "coreapi must be installed to use `get_schema_fields()`" + assert ( + compat.coreschema is not None + ), "coreschema must be installed to use `get_schema_fields()`" try: queryset = view.get_queryset() @@ -130,16 +115,26 @@ def get_schema_fields(self, view): filterset_class = self.get_filterset_class(view, queryset) - return [] if not filterset_class else [ - compat.coreapi.Field( - name=field_name, - required=field.extra['required'], - location='query', - schema=self.get_coreschema_field(field) - ) for field_name, field in filterset_class.base_filters.items() - ] + return ( + [] + if not filterset_class + else [ + compat.coreapi.Field( + name=field_name, + required=field.extra["required"], + location="query", + schema=self.get_coreschema_field(field), + ) + for field_name, field in filterset_class.base_filters.items() + ] + ) def get_schema_operation_parameters(self, view): + from django_filters import RemovedInDjangoFilter25Warning + warnings.warn( + "Built-in schema generation is deprecated. Use drf-spectacular.", + category=RemovedInDjangoFilter25Warning, + ) try: queryset = view.get_queryset() except Exception: @@ -156,15 +151,15 @@ def get_schema_operation_parameters(self, view): parameters = [] for field_name, field in filterset_class.base_filters.items(): parameter = { - 'name': field_name, - 'required': field.extra['required'], - 'in': 'query', - 'description': field.label if field.label is not None else field_name, - 'schema': { - 'type': 'string', + "name": field_name, + "required": field.extra["required"], + "in": "query", + "description": field.label if field.label is not None else field_name, + "schema": { + "type": "string", }, } - if field.extra and 'choices' in field.extra: - parameter['schema']['enum'] = [c[0] for c in field.extra['choices']] + if field.extra and "choices" in field.extra: + parameter["schema"]["enum"] = [c[0] for c in field.extra["choices"]] parameters.append(parameter) return parameters diff --git a/django_filters/rest_framework/filters.py b/django_filters/rest_framework/filters.py index 4c5755e9a..e2c8f10e3 100644 --- a/django_filters/rest_framework/filters.py +++ b/django_filters/rest_framework/filters.py @@ -8,6 +8,6 @@ class BooleanFilter(filters.BooleanFilter): def __init__(self, *args, **kwargs): - kwargs.setdefault('widget', BooleanWidget) + kwargs.setdefault("widget", BooleanWidget) super().__init__(*args, **kwargs) diff --git a/django_filters/rest_framework/filterset.py b/django_filters/rest_framework/filterset.py index 8c4230422..c27f77b8b 100644 --- a/django_filters/rest_framework/filterset.py +++ b/django_filters/rest_framework/filterset.py @@ -9,11 +9,13 @@ from .filters import BooleanFilter, IsoDateTimeFilter FILTER_FOR_DBFIELD_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS) -FILTER_FOR_DBFIELD_DEFAULTS.update({ - models.DateTimeField: {'filter_class': IsoDateTimeFilter}, - models.BooleanField: {'filter_class': BooleanFilter}, - models.NullBooleanField: {'filter_class': BooleanFilter}, -}) +FILTER_FOR_DBFIELD_DEFAULTS.update( + { + models.DateTimeField: {"filter_class": IsoDateTimeFilter}, + models.BooleanField: {"filter_class": BooleanFilter}, + models.NullBooleanField: {"filter_class": BooleanFilter}, + } +) class FilterSet(filterset.FilterSet): @@ -28,11 +30,10 @@ def form(self): from crispy_forms.layout import Layout, Submit layout_components = list(form.fields.keys()) + [ - Submit('', _('Submit'), css_class='btn-default'), + Submit("", _("Submit"), css_class="btn-default"), ] helper = FormHelper() - helper.form_method = 'GET' - helper.template_pack = 'bootstrap3' + helper.form_method = "GET" helper.layout = Layout(*layout_components) form.helper = helper diff --git a/django_filters/utils.py b/django_filters/utils.py index 125910c70..b8a48e4b4 100644 --- a/django_filters/utils.py +++ b/django_filters/utils.py @@ -1,6 +1,8 @@ +import datetime import warnings from collections import OrderedDict +import django from django.conf import settings from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models @@ -20,10 +22,10 @@ def deprecate(msg, level_modifier=0): class MigrationNotice(DeprecationWarning): - url = 'https://django-filter.readthedocs.io/en/master/guide/migration.html' + url = "https://django-filter.readthedocs.io/en/main/guide/migration.html" def __init__(self, message): - super().__init__('%s See: %s' % (message, self.url)) + super().__init__("%s See: %s" % (message, self.url)) class RenameAttributesBase(type): @@ -36,6 +38,7 @@ class RenameAttributesBase(type): This is conceptually based on `django.utils.deprecation.RenameMethodsBase`. """ + renamed_attributes = () def __new__(metacls, name, bases, attrs): @@ -45,8 +48,8 @@ def __new__(metacls, name, bases, attrs): old_attrs = {name: attrs.pop(name) for name in old_names} # get a handle to any accessors defined on the class - cls_getattr = attrs.pop('__getattr__', None) - cls_setattr = attrs.pop('__setattr__', None) + cls_getattr = attrs.pop("__getattr__", None) + cls_setattr = attrs.pop("__setattr__", None) new_class = super().__new__(metacls, name, bases, attrs) @@ -54,7 +57,7 @@ def __getattr__(self, name): name = type(self).get_name(name) if cls_getattr is not None: return cls_getattr(self, name) - elif hasattr(super(new_class, self), '__getattr__'): + elif hasattr(super(new_class, self), "__getattr__"): return super(new_class, self).__getattr__(name) return self.__getattribute__(name) @@ -82,9 +85,12 @@ def get_name(metacls, name): old_name, new_name, deprecation_warning = renamed_attribute if old_name == name: - warnings.warn("`%s.%s` attribute should be renamed `%s`." - % (metacls.__name__, old_name, new_name), - deprecation_warning, 3) + warnings.warn( + "`%s.%s` attribute should be renamed `%s`." + % (metacls.__name__, old_name, new_name), + deprecation_warning, + 3, + ) return new_name return name @@ -120,9 +126,10 @@ def get_all_model_fields(model): opts = model._meta return [ - f.name for f in sorted(opts.fields + opts.many_to_many) - if not isinstance(f, models.AutoField) and - not (getattr(f.remote_field, 'parent_link', False)) + f.name + for f in sorted(opts.fields + opts.many_to_many) + if not isinstance(f, models.AutoField) + and not (getattr(f.remote_field, "parent_link", False)) ] @@ -164,10 +171,19 @@ def get_field_parts(model, field_name): return None fields.append(field) - if isinstance(field, RelatedField): - opts = field.remote_field.model._meta - elif isinstance(field, ForeignObjectRel): - opts = field.related_model._meta + try: + if isinstance(field, RelatedField): + opts = field.remote_field.model._meta + elif isinstance(field, ForeignObjectRel): + opts = field.related_model._meta + except AttributeError: + # Lazy relationships are not resolved until registry is populated. + raise RuntimeError( + "Unable to resolve relationship `%s` for `%s`. Django is most " + "likely not initialized, and its apps registry not populated. " + "Ensure Django has finished setup before loading `FilterSet`s." + % (field_name, model._meta.label) + ) return fields @@ -205,7 +221,7 @@ def resolve_field(model_field, lookup_expr): # the name as transform, and do an Exact lookup against # it. lhs = query.try_transform(*args) - final_lookup = lhs.get_lookup('exact') + final_lookup = lhs.get_lookup("exact") return lhs.output_field, final_lookup.lookup_name lhs = query.try_transform(*args) lookups = lookups[1:] @@ -215,9 +231,22 @@ def resolve_field(model_field, lookup_expr): def handle_timezone(value, is_dst=None): if settings.USE_TZ and timezone.is_naive(value): - return timezone.make_aware(value, timezone.get_current_timezone(), is_dst) + # Pre-4.x versions of Django have is_dst. Later Django versions have + # zoneinfo where the is_dst argument has no meaning. is_dst will be + # removed in the 5.x series. + # + # On intermediate versions, the default is to use zoneinfo, but pytz + # is still available under USE_DEPRECATED_PYTZ, and is_dst is + # meaningful there. Under those versions we should only use is_dst + # if USE_DEPRECATED_PYTZ is present and True; otherwise, we will cause + # deprecation warnings, and we should not. See #1580. + # + # This can be removed once 3.2 is no longer supported upstream. + if django.VERSION < (4, 0) or (django.VERSION < (5, 0) and settings.USE_DEPRECATED_PYTZ): + return timezone.make_aware(value, timezone.get_current_timezone(), is_dst) + return timezone.make_aware(value, timezone.get_current_timezone()) elif not settings.USE_TZ and timezone.is_aware(value): - return timezone.make_naive(value, timezone.utc) + return timezone.make_naive(value, datetime.timezone.utc) return value @@ -234,23 +263,23 @@ def verbose_field_name(model, field_name): """ if field_name is None: - return '[invalid name]' + return "[invalid name]" parts = get_field_parts(model, field_name) if not parts: - return '[invalid name]' + return "[invalid name]" names = [] for part in parts: if isinstance(part, ForeignObjectRel): if part.related_name: - names.append(part.related_name.replace('_', ' ')) + names.append(part.related_name.replace("_", " ")) else: - return '[invalid name]' + return "[invalid name]" else: names.append(force_str(part.verbose_name)) - return ' '.join(names) + return " ".join(names) def verbose_lookup_expr(lookup_expr): @@ -277,7 +306,7 @@ def verbose_lookup_expr(lookup_expr): for lookup in lookup_expr.split(LOOKUP_SEP) ] - return ' '.join(lookups) + return " ".join(lookups) def label_for_filter(model, field_name, lookup_expr, exclude=False): @@ -291,14 +320,14 @@ def label_for_filter(model, field_name, lookup_expr, exclude=False): """ name = verbose_field_name(model, field_name) - verbose_expression = [_('exclude'), name] if exclude else [name] + verbose_expression = [_("exclude"), name] if exclude else [name] # iterable lookups indicate a LookupTypeField, which should not be verbose if isinstance(lookup_expr, str): verbose_expression += [verbose_lookup_expr(lookup_expr)] verbose_expression = [force_str(part) for part in verbose_expression if part] - verbose_expression = capfirst(' '.join(verbose_expression)) + verbose_expression = capfirst(" ".join(verbose_expression)) return verbose_expression @@ -312,8 +341,13 @@ def translate_validation(error_dict): from rest_framework.exceptions import ErrorDetail, ValidationError exc = OrderedDict( - (key, [ErrorDetail(e.message % (e.params or ()), code=e.code) - for e in error_list]) + ( + key, + [ + ErrorDetail(e.message % (e.params or ()), code=e.code) + for e in error_list + ], + ) for key, error_list in error_dict.as_data().items() ) diff --git a/django_filters/views.py b/django_filters/views.py index e9160ad93..c24f9abac 100644 --- a/django_filters/views.py +++ b/django_filters/views.py @@ -2,25 +2,18 @@ from django.views.generic import View from django.views.generic.list import ( MultipleObjectMixin, - MultipleObjectTemplateResponseMixin + MultipleObjectTemplateResponseMixin, ) from .constants import ALL_FIELDS from .filterset import filterset_factory -from .utils import MigrationNotice, RenameAttributesBase -# TODO: remove metaclass in 2.1 -class FilterMixinRenames(RenameAttributesBase): - renamed_attributes = ( - ('filter_fields', 'filterset_fields', MigrationNotice), - ) - - -class FilterMixin(metaclass=FilterMixinRenames): +class FilterMixin: """ A mixin that provides a way to show and handle a FilterSet in a request. """ + filterset_class = None filterset_fields = ALL_FIELDS strict = True @@ -49,20 +42,24 @@ def get_filterset_kwargs(self, filterset_class): Returns the keyword arguments for instantiating the filterset. """ kwargs = { - 'data': self.request.GET or None, - 'request': self.request, + "data": self.request.GET or None, + "request": self.request, } try: - kwargs.update({ - 'queryset': self.get_queryset(), - }) + kwargs.update( + { + "queryset": self.get_queryset(), + } + ) except ImproperlyConfigured: # ignore the error here if the filterset has a model defined # to acquire a queryset from if filterset_class._meta.model is None: - msg = ("'%s' does not define a 'model' and the view '%s' does " - "not return a valid queryset from 'get_queryset'. You " - "must fix one of them.") + msg = ( + "'%s' does not define a 'model' and the view '%s' does " + "not return a valid queryset from 'get_queryset'. You " + "must fix one of them." + ) args = (filterset_class.__name__, self.__class__.__name__) raise ImproperlyConfigured(msg % args) return kwargs @@ -72,18 +69,22 @@ def get_strict(self): class BaseFilterView(FilterMixin, MultipleObjectMixin, View): - def get(self, request, *args, **kwargs): filterset_class = self.get_filterset_class() self.filterset = self.get_filterset(filterset_class) - if not self.filterset.is_bound or self.filterset.is_valid() or not self.get_strict(): + if ( + not self.filterset.is_bound + or self.filterset.is_valid() + or not self.get_strict() + ): self.object_list = self.filterset.qs else: self.object_list = self.filterset.queryset.none() - context = self.get_context_data(filter=self.filterset, - object_list=self.object_list) + context = self.get_context_data( + filter=self.filterset, object_list=self.object_list + ) return self.render_to_response(context) @@ -93,24 +94,36 @@ class FilterView(MultipleObjectTemplateResponseMixin, BaseFilterView): `self.queryset`. `self.queryset` can actually be any iterable of items, not just a queryset. """ - template_name_suffix = '_filter' + + template_name_suffix = "_filter" -def object_filter(request, model=None, queryset=None, template_name=None, - extra_context=None, context_processors=None, - filter_class=None): +def object_filter( + request, + model=None, + queryset=None, + template_name=None, + extra_context=None, + context_processors=None, + filter_class=None, +): class ECFilterView(FilterView): """Handle the extra_context from the functional object_filter view""" + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - extra_context = self.kwargs.get('extra_context') or {} + extra_context = self.kwargs.get("extra_context") or {} for k, v in extra_context.items(): if callable(v): v = v() context[k] = v return context - kwargs = dict(model=model, queryset=queryset, template_name=template_name, - filterset_class=filter_class) + kwargs = dict( + model=model, + queryset=queryset, + template_name=template_name, + filterset_class=filter_class, + ) view = ECFilterView.as_view(**kwargs) return view(request, extra_context=extra_context) diff --git a/django_filters/widgets.py b/django_filters/widgets.py index 14b2142d6..8d3583310 100644 --- a/django_filters/widgets.py +++ b/django_filters/widgets.py @@ -25,17 +25,17 @@ def value_from_datadict(self, data, files, name): return value def render(self, name, value, attrs=None, choices=(), renderer=None): - if not hasattr(self, 'data'): + if not hasattr(self, "data"): self.data = {} if value is None: - value = '' + value = "" final_attrs = self.build_attrs(self.attrs, extra_attrs=attrs) - output = ['' % flatatt(final_attrs)] + output = ["" % flatatt(final_attrs)] options = self.render_options(choices, [value], name) if options: output.append(options) - output.append('') - return mark_safe('\n'.join(output)) + output.append("") + return mark_safe("\n".join(output)) def render_options(self, choices, selected_choices, name): selected_choices = set(force_str(v) for v in selected_choices) @@ -43,16 +43,16 @@ def render_options(self, choices, selected_choices, name): for option_value, option_label in chain(self.choices, choices): if isinstance(option_label, (list, tuple)): for option in option_label: - output.append( - self.render_option(name, selected_choices, *option)) + output.append(self.render_option(name, selected_choices, *option)) else: output.append( - self.render_option(name, selected_choices, - option_value, option_label)) - return '\n'.join(output) + self.render_option( + name, selected_choices, option_value, option_label + ) + ) + return "\n".join(output) - def render_option(self, name, selected_choices, - option_value, option_label): + def render_option(self, name, selected_choices, option_value, option_label): option_value = force_str(option_value) if option_label == BLANK_CHOICE_DASH[0][1]: option_label = _("All") @@ -64,9 +64,9 @@ def render_option(self, name, selected_choices, except AttributeError: url = urlencode(data) return self.option_string() % { - 'attrs': selected and ' class="selected"' or '', - 'query_string': url, - 'label': force_str(option_label) + "attrs": selected and ' class="selected"' or "", + "query_string": url, + "label": force_str(option_label), } def option_string(self): @@ -80,6 +80,7 @@ class SuffixedMultiWidget(forms.MultiWidget): - Suffixes must be unique. - There must be the same number of suffixes as fields. """ + suffixes = [] def __init__(self, *args, **kwargs): @@ -89,12 +90,12 @@ def __init__(self, *args, **kwargs): assert len(self.suffixes) == len(set(self.suffixes)) def suffixed(self, name, suffix): - return '_'.join([name, suffix]) if suffix else name + return "_".join([name, suffix]) if suffix else name def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - for subcontext, suffix in zip(context['widget']['subwidgets'], self.suffixes): - subcontext['name'] = self.suffixed(name, suffix) + for subcontext, suffix in zip(context["widget"]["subwidgets"], self.suffixes): + subcontext["name"] = self.suffixed(name, suffix) return context @@ -112,7 +113,7 @@ def value_omitted_from_data(self, data, files, name): def replace_name(self, output, index): result = search(r'name="(?P.*)_%d"' % index, output) - name = result.group('name') + name = result.group("name") name = self.suffixed(name, self.suffixes[index]) name = 'name="%s"' % name @@ -125,8 +126,8 @@ def decompress(self, value): class RangeWidget(SuffixedMultiWidget): - template_name = 'django_filters/widgets/multiwidget.html' - suffixes = ['min', 'max'] + template_name = "django_filters/widgets/multiwidget.html" + suffixes = ["min", "max"] def __init__(self, attrs=None): widgets = (forms.TextInput, forms.TextInput) @@ -139,11 +140,11 @@ def decompress(self, value): class DateRangeWidget(RangeWidget): - suffixes = ['after', 'before'] + suffixes = ["after", "before"] class LookupChoiceWidget(SuffixedMultiWidget): - suffixes = [None, 'lookup'] + suffixes = [None, "lookup"] def decompress(self, value): if value is None: @@ -156,22 +157,16 @@ class BooleanWidget(forms.Select): This can be used for AJAX queries that pass true/false from JavaScript's internal types through. """ + def __init__(self, attrs=None): - choices = (('', _('Unknown')), - ('true', _('Yes')), - ('false', _('No'))) + choices = (("", _("Unknown")), ("true", _("Yes")), ("false", _("No"))) super().__init__(attrs, choices) def render(self, name, value, attrs=None, renderer=None): try: - value = { - True: 'true', - False: 'false', - '1': 'true', - '0': 'false' - }[value] + value = {True: "true", False: "false", "1": "true", "0": "false"}[value] except KeyError: - value = '' + value = "" return super().render(name, value, attrs, renderer=renderer) def value_from_datadict(self, data, files, name): @@ -180,10 +175,10 @@ def value_from_datadict(self, data, files, name): value = value.lower() return { - '1': True, - '0': False, - 'true': True, - 'false': False, + "1": True, + "0": False, + "true": True, + "false": False, True: True, False: False, }.get(value, None) @@ -208,13 +203,13 @@ def value_from_datadict(self, data, files, name): value = super().value_from_datadict(data, files, name) if value is not None: - if value == '': # empty value should parse as an empty list + if value == "": # empty value should parse as an empty list return [] if isinstance(value, list): # since django.forms.widgets.SelectMultiple tries to use getlist # if available, we should return value if it's already an array return value - return value.split(',') + return value.split(",") return None def render(self, name, value, attrs=None, renderer=None): @@ -223,13 +218,13 @@ def render(self, name, value, attrs=None, renderer=None): if len(value) <= 1: # delegate to main widget (Select, etc...) if not multiple values - value = value[0] if value else '' + value = value[0] if value else "" return super().render(name, value, attrs, renderer=renderer) # if we have multiple values, we need to force render as a text input # (otherwise, the additional values are lost) value = [force_str(self.surrogate.format_value(v)) for v in value] - value = ','.join(list(value)) + value = ",".join(list(value)) return self.surrogate.render(name, value, attrs, renderer=renderer) @@ -255,13 +250,14 @@ class QueryArrayWidget(BaseCSVWidget, forms.TextInput): def value_from_datadict(self, data, files, name): if not isinstance(data, MultiValueDict): + data = data.copy() for key, value in data.items(): # treat value as csv string: ?foo=1,2 if isinstance(value, str): - data[key] = [x.strip() for x in value.rstrip(',').split(',') if x] + data[key] = [x.strip() for x in value.rstrip(",").split(",") if x] data = MultiValueDict(data) - values_list = data.getlist(name, data.getlist('%s[]' % name)) or [] + values_list = data.getlist(name, data.getlist("%s[]" % name)) or [] # apparently its an array, so no need to process it's values as csv # ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2] diff --git a/docs/conf.py b/docs/conf.py index 5de3b5e91..7bc023380 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,39 +11,40 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys from django_filters import __version__ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.txt' +source_suffix = ".txt" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'django-filter' -copyright = u'2020, Alex Gaynor, Carlton Gibson and others.' +project = u"django-filter" +copyright = u"2022, Alex Gaynor, Carlton Gibson and others." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -56,158 +57,161 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +# html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-filterdoc' +htmlhelp_basename = "django-filterdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-filter.tex', u'django-filter Documentation', - u'Alex Gaynor and others.', 'manual'), + ( + "index", + "django-filter.tex", + u"django-filter Documentation", + u"Alex Gaynor and others.", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -215,12 +219,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-filter', u'django-filter Documentation', - [u'Alex Gaynor and others.'], 1) + ( + "index", + "django-filter", + u"django-filter Documentation", + [u"Alex Gaynor and others."], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -229,28 +238,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-filter', u'django-filter Documentation', - u'Alex Gaynor and others.', 'django-filter', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "django-filter", + u"django-filter Documentation", + u"Alex Gaynor and others.", + "django-filter", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# see: -# https://github.com/snide/sphinx_rtd_theme#using-this-theme-locally-then-building-on-read-the-docs -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - -# only import and set the theme if we're building docs locally -if not on_rtd: - import sphinx_rtd_theme - - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# texinfo_show_urls = 'footnote' diff --git a/docs/guide/install.txt b/docs/guide/install.txt index 601765a6a..0ac4cf16f 100644 --- a/docs/guide/install.txt +++ b/docs/guide/install.txt @@ -21,14 +21,9 @@ Then add ``'django_filters'`` to your ``INSTALLED_APPS``. Requirements ------------ -Django-filter is tested against all supported versions of Python and `Django`__, -as well as the latest version of Django REST Framework (`DRF`__). +Django-filter requires a current version of `Django`__ and is tested against +all supported versions of Python, as well as the latest version of Django REST +Framework (`DRF`__). -__ https://www.djangoproject.com/download/ +__ https://www.djangoproject.com/download/#supported-versions __ http://www.django-rest-framework.org/ - - - -* **Python**: 3.5, 3.6, 3.7, 3.8 -* **Django**: 2.2, 3.0, 3.1 -* **DRF**: 3.10+ diff --git a/docs/guide/rest_framework.txt b/docs/guide/rest_framework.txt index 04bbcade1..477d8cb9b 100644 --- a/docs/guide/rest_framework.txt +++ b/docs/guide/rest_framework.txt @@ -1,3 +1,5 @@ +.. _drf integration: + ==================== Integration with DRF ==================== @@ -7,7 +9,6 @@ Integration with `Django Rest Framework`__ is provided through a DRF-specific `` __ http://www.django-rest-framework.org/ __ http://www.django-rest-framework.org/api-guide/filtering/ - Quickstart ---------- @@ -69,7 +70,7 @@ To enable filtering with a ``FilterSet``, add it to the ``filterset_class`` para class Meta: model = Product - fields = ['category', 'in_stock', 'min_price', 'max_price'] + fields = ['category', 'in_stock'] class ProductList(generics.ListAPIView): diff --git a/docs/guide/tips.txt b/docs/guide/tips.txt index e6ceb5036..715e9e540 100644 --- a/docs/guide/tips.txt +++ b/docs/guide/tips.txt @@ -28,7 +28,7 @@ lookup type. Under the hood, this will incorrectly be resolved as: .. code-block:: python - Produce.objects.filter(price__gt__exact=value) + Product.objects.filter(price__gt__exact=value) The above will most likely generate a ``FieldError``. The correct configuration would be: @@ -38,6 +38,17 @@ would be: class ProductFilter(django_filters.FilterSet): price__gt = django_filters.NumberFilter(field_name='price', lookup_expr='gt') +When using ``filterset_fields``, you can also add the ``lookup_expr`` in the +dict of fields like so: + +.. code-block:: python + + # ... ModelViewSet with DjangoFilterBackend in filter_backends ... + + filterset_fields = { + "price": ["gt", "exact"], + } + Missing ``lookup_expr`` for text search filters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -215,7 +226,7 @@ Filtering by relative times Given a model with a timestamp field, it may be useful to filter based on relative times. For instance, perhaps we want to get data from the past *n* hours. -This could be accomplished the with a ``NumberFilter`` that invokes a custom method. +This could be accomplished with a ``NumberFilter`` that invokes a custom method. .. code-block:: python diff --git a/docs/guide/usage.txt b/docs/guide/usage.txt index da085478a..9f7e938da 100644 --- a/docs/guide/usage.txt +++ b/docs/guide/usage.txt @@ -8,8 +8,8 @@ our users filter which products they see on a list page. .. note:: - If you're using django-filter with Django Rest Framework, it's - recommended that you read the integration docs after this guide. + If you're using django-filter with Django Rest Framework, it's recommended + that you read the :ref:`drf integration` docs after this guide. The model --------- @@ -20,10 +20,10 @@ Let's start with our model:: class Product(models.Model): name = models.CharField(max_length=255) - price = models.DecimalField() + price = models.DecimalField(max_digits=5, decimal_places=2) description = models.TextField() release_date = models.DateField() - manufacturer = models.ForeignKey(Manufacturer) + manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE) The filter ---------- @@ -331,7 +331,7 @@ to it as the class based view:: from myapp.models import Product urlpatterns = [ - path("list/', object_filter, {'model': Product}, name="product-list), + path("list/", object_filter, {'model': Product}, name="product-list"), ] The needed template and its context variables will also be the same as the diff --git a/docs/ref/filters.txt b/docs/ref/filters.txt index fcf0974dc..d5c8711b5 100644 --- a/docs/ref/filters.txt +++ b/docs/ref/filters.txt @@ -35,8 +35,8 @@ year part ``year__gt``. .. _keyword-only-arguments: -Keyword-only Arguments: ------------------------ +Keyword-only Arguments +---------------------- The following are optional arguments that can be used to modify the behavior of all filters. diff --git a/docs/ref/filterset.txt b/docs/ref/filterset.txt index fef637452..139f4190c 100644 --- a/docs/ref/filterset.txt +++ b/docs/ref/filterset.txt @@ -72,7 +72,7 @@ include both transforms and lookups, as detailed in the `lookup reference`__. __ https://docs.djangoproject.com/en/stable/ref/models/lookups/#module-django.db.models.lookups Note that it is **not** necessary to include declared filters in a ``fields`` -list - doing so will have no effect - and including declarative aliases in a +list - doing so will only affect the order in which fields appear on a FilterSet's form. Including declarative aliases in a ``fields`` dict will raise an error. .. code-block:: python diff --git a/docs/ref/widgets.txt b/docs/ref/widgets.txt index b9b34d14f..31543d3f1 100644 --- a/docs/ref/widgets.txt +++ b/docs/ref/widgets.txt @@ -54,7 +54,7 @@ well as type conversion. This widget is used with ``RangeFilter`` and its subclasses. It generates two form input elements which generally act as start/end values in a range. -Under the hood, it is Django's ``forms.TextInput`` widget and excepts +Under the hood, it is Django's ``forms.TextInput`` widget and accepts the same arguments and values. To use it, pass it to ``widget`` argument of a ``RangeField``: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..e58d7c673 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "django-filter" +authors = [{name = "Alex Gaynor", email = "alex.gaynor@gmail.com"}] +maintainers = [{name = "Carlton Gibson", email = "carlton.gibson@noumenal.es"}] +license = {text = "BSD"} +description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +requires-python = ">=3.7" +dependencies = ["Django>=3.2"] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/carltongibson/django-filter/tree/main" +Documentation = "https://django-filter.readthedocs.io/en/main/" +Changelog = "https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst" +"Bug Tracker" = "https://github.com/carltongibson/django-filter/issues" +"Source Code" = "https://github.com/carltongibson/django-filter" + +[tool.setuptools] +zip-safe = false +include-package-data = true +license-files = ["LICENSE"] + +[tool.setuptools.packages.find] +exclude = ["tests*"] +namespaces = false + +[tool.isort] +profile = "black" +skip = [".tox"] +known_third_party = ["django", "pytz", "rest_framework"] +known_first_party = ["django_filters"] + +[tool.flit.module] +name = "django_filters" diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 000000000..303d17f6b --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,3 @@ +Sphinx +furo +. diff --git a/requirements/maintainer.txt b/requirements/maintainer.txt index baf75e0b7..e298a5828 100644 --- a/requirements/maintainer.txt +++ b/requirements/maintainer.txt @@ -1,25 +1,3 @@ -alabaster==0.7.7 -argh==0.26.1 -Babel==2.2.0 -backports.ssl-match-hostname==3.4.0.2 -certifi==2015.9.6.2 -docutils==0.12 -funcsigs==0.4 -Jinja2>=2.10.1 -livereload==2.4.0 -MarkupSafe==0.23 -pathtools==0.1.2 -pbr==1.7.0 -pkginfo==1.2.1 -Pygments==2.1.3 -pytz==2016.6.1 -PyYAML==5.1 -requests==2.20.0 -six==1.9.0 -snowballstemmer==1.2.1 -Sphinx==1.3.6 -sphinx-autobuild==0.6.0 -sphinx-rtd-theme==0.1.9 -tornado==4.2.1 twine -watchdog==0.8.3 +wheel +Sphinx \ No newline at end of file diff --git a/requirements/test-ci.txt b/requirements/test-ci.txt index f10a9c33a..2f787bf94 100644 --- a/requirements/test-ci.txt +++ b/requirements/test-ci.txt @@ -1,8 +1,6 @@ markdown -coreapi django-crispy-forms coverage -mock pytz unittest-xml-reporting diff --git a/runshell.py b/runshell.py index 48af2821a..652d5e0f3 100755 --- a/runshell.py +++ b/runshell.py @@ -1,13 +1,15 @@ #!/usr/bin/env python import os import sys + from django.core.management import execute_from_command_line def runshell(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") - execute_from_command_line(sys.argv[:1] + ['migrate', '--noinput', '-v', '0']) - execute_from_command_line(sys.argv[:1] + ['shell'] + sys.argv[1:]) + execute_from_command_line(sys.argv[:1] + ["migrate", "--noinput", "-v", "0"]) + execute_from_command_line(sys.argv[:1] + ["shell"] + sys.argv[1:]) + -if __name__ == '__main__': +if __name__ == "__main__": runshell() diff --git a/runtests.py b/runtests.py index 0de8c7707..cdf26b646 100755 --- a/runtests.py +++ b/runtests.py @@ -1,14 +1,15 @@ #!/usr/bin/env python import os import sys + from django.core.management import execute_from_command_line def runtests(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") - argv = sys.argv[:1] + ['test'] + sys.argv[1:] + argv = sys.argv[:1] + ["test"] + sys.argv[1:] execute_from_command_line(argv) -if __name__ == '__main__': +if __name__ == "__main__": runtests() diff --git a/setup.cfg b/setup.cfg index 3a8d64827..a75362422 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,14 +1,3 @@ -[metadata] -license-file = LICENSE - -[isort] -skip=.tox -atomic=true -multi_line_output=3 -extra_standard_library=mock -known_third_party=django,pytz,rest_framework -known_first_party=django_filters - [flake8] max_line_length = 120 max_complexity = 10 diff --git a/setup.py b/setup.py deleted file mode 100644 index 1a7e626cc..000000000 --- a/setup.py +++ /dev/null @@ -1,65 +0,0 @@ -import os -import sys -from setuptools import setup, find_packages - -# FIXME: Main module requires django to be present, so cannot run setup.py in -# clean environment. -# from django_filters import __version__ -__version__ = '2.4.0' - -f = open('README.rst') -readme = f.read() -f.close() - -if sys.argv[-1] == 'publish': - if os.system("pip freeze | grep wheel"): - print("wheel not installed.\nUse `pip install wheel`.\nExiting.") - sys.exit() - if os.system("pip freeze | grep twine"): - print("twine not installed.\nUse `pip install twine`.\nExiting.") - sys.exit() - os.system("python setup.py sdist bdist_wheel") - os.system("twine upload dist/*") - print("You probably want to also tag the version now:") - print(" git tag -a %s -m 'version %s'" % (__version__, __version__)) - print(" git push --tags") - sys.exit() - -setup( - name='django-filter', - version=__version__, - description=('Django-filter is a reusable Django application for allowing' - ' users to filter querysets dynamically.'), - long_description=readme, - author='Alex Gaynor', - author_email='alex.gaynor@gmail.com', - maintainer='Carlton Gibson', - maintainer_email='carlton.gibson@noumenal.es', - url='https://github.com/carltongibson/django-filter/tree/master', - packages=find_packages(exclude=['tests*']), - include_package_data=True, - license='BSD', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - ], - zip_safe=False, - python_requires='>=3.5', - install_requires=[ - 'Django>=2.2', - ], -) diff --git a/tests/models.py b/tests/models.py index 2ab2af002..e1a391b21 100644 --- a/tests/models.py +++ b/tests/models.py @@ -7,9 +7,9 @@ ADMIN = 2 STATUS_CHOICES = ( - (REGULAR, 'Regular'), - (MANAGER, 'Manager'), - (ADMIN, 'Admin'), + (REGULAR, "Regular"), + (MANAGER, "Manager"), + (ADMIN, "Admin"), ) @@ -27,20 +27,20 @@ class SubnetMaskField(models.Field): description = "Subnet Mask" def __init__(self, *args, **kwargs): - kwargs['max_length'] = 15 + kwargs["max_length"] = 15 models.Field.__init__(self, *args, **kwargs) def get_internal_type(self): return "GenericIPAddressField" def formfield(self, **kwargs): - defaults = {'form_class': forms.GenericIPAddressField} + defaults = {"form_class": forms.GenericIPAddressField} defaults.update(kwargs) return super().formfield(**defaults) class User(models.Model): - username = models.CharField(_('username'), max_length=255) + username = models.CharField(_("username"), max_length=255) first_name = SubCharField(max_length=100) last_name = SubSubCharField(max_length=100) @@ -49,23 +49,25 @@ class User(models.Model): is_active = models.BooleanField(default=False) is_employed = models.BooleanField(null=True, default=False) - favorite_books = models.ManyToManyField('Book', related_name='lovers') + favorite_books = models.ManyToManyField("Book", related_name="lovers") def __str__(self): return self.username class ManagerGroup(models.Model): - users = models.ManyToManyField(User, - limit_choices_to={'is_active': True}, - related_name='member_of') - manager = models.ForeignKey(User, - limit_choices_to=lambda: {'status': MANAGER}, - related_name='manager_of', - on_delete=models.CASCADE) + users = models.ManyToManyField( + User, limit_choices_to={"is_active": True}, related_name="member_of" + ) + manager = models.ForeignKey( + User, + limit_choices_to=lambda: {"status": MANAGER}, + related_name="manager_of", + on_delete=models.CASCADE, + ) def __str__(self): - return self.manager.name + ' group' + return self.manager.name + " group" class AdminUser(User): @@ -78,7 +80,7 @@ def __str__(self): class Comment(models.Model): text = models.TextField() - author = models.ForeignKey(User, related_name='comments', on_delete=models.CASCADE) + author = models.ForeignKey(User, related_name="comments", on_delete=models.CASCADE) date = models.DateField() time = models.TimeField() @@ -88,7 +90,7 @@ def __str__(self): class Article(models.Model): - name = models.CharField(verbose_name='title', max_length=200, blank=True) + name = models.CharField(verbose_name="title", max_length=200, blank=True) published = models.DateTimeField() author = models.ForeignKey(User, null=True, on_delete=models.CASCADE) @@ -132,17 +134,19 @@ def __str__(self): return self.name class Meta: - ordering = ['name'] + ordering = ["name"] class Location(models.Model): - company = models.ForeignKey(Company, related_name='locations', on_delete=models.CASCADE) + company = models.ForeignKey( + Company, related_name="locations", on_delete=models.CASCADE + ) name = models.CharField(max_length=100) zip_code = models.CharField(max_length=10) open_days = models.CharField(max_length=7) def __str__(self): - return '%s: %s' % (self.company.name, self.name) + return "%s: %s" % (self.company.name, self.name) class Account(models.Model): @@ -152,7 +156,9 @@ class Account(models.Model): class Profile(models.Model): - account = models.OneToOneField(Account, related_name='profile', on_delete=models.CASCADE) + account = models.OneToOneField( + Account, related_name="profile", on_delete=models.CASCADE + ) likes_coffee = models.BooleanField(default=False) likes_tea = models.BooleanField(default=False) @@ -163,14 +169,14 @@ class BankAccount(Account): class Node(models.Model): name = models.CharField(max_length=20) - adjacents = models.ManyToManyField('self') + adjacents = models.ManyToManyField("self") class DirectedNode(models.Model): name = models.CharField(max_length=20) - outbound_nodes = models.ManyToManyField('self', - symmetrical=False, - related_name='inbound_nodes') + outbound_nodes = models.ManyToManyField( + "self", symmetrical=False, related_name="inbound_nodes" + ) class Worker(models.Model): @@ -181,14 +187,14 @@ class HiredWorker(models.Model): salary = models.IntegerField() hired_on = models.DateField() worker = models.ForeignKey(Worker, on_delete=models.CASCADE) - business = models.ForeignKey('Business', on_delete=models.CASCADE) + business = models.ForeignKey("Business", on_delete=models.CASCADE) class Business(models.Model): name = models.CharField(max_length=100) - employees = models.ManyToManyField(Worker, - through=HiredWorker, - related_name='employers') + employees = models.ManyToManyField( + Worker, through=HiredWorker, related_name="employers" + ) class UUIDTestModel(models.Model): @@ -201,5 +207,6 @@ class SpacewalkRecord(models.Model): See: https://en.wikipedia.org/wiki/List_of_cumulative_spacewalk_records """ + astronaut = models.CharField(max_length=100) duration = models.DurationField() diff --git a/tests/rest_framework/__init__.py b/tests/rest_framework/__init__.py index 488e6f11e..8bfe83a19 100644 --- a/tests/rest_framework/__init__.py +++ b/tests/rest_framework/__init__.py @@ -1 +1 @@ -default_app_config = 'tests.rest_framework.apps.RestFrameworkTestConfig' +default_app_config = "tests.rest_framework.apps.RestFrameworkTestConfig" diff --git a/tests/rest_framework/apps.py b/tests/rest_framework/apps.py index 0a1efc13f..1fb9cb6a8 100644 --- a/tests/rest_framework/apps.py +++ b/tests/rest_framework/apps.py @@ -1,8 +1,7 @@ - from django.apps import AppConfig class RestFrameworkTestConfig(AppConfig): - name = 'tests.rest_framework' - label = 'drf_test_app' + name = "tests.rest_framework" + label = "drf_test_app" verbose_name = "Rest Framework Test App" diff --git a/tests/rest_framework/models.py b/tests/rest_framework/models.py index a6f5e8a1c..3ae91db58 100644 --- a/tests/rest_framework/models.py +++ b/tests/rest_framework/models.py @@ -1,4 +1,3 @@ - from django.db import models from django.utils.translation import gettext_lazy as _ @@ -7,7 +6,7 @@ class BasicModel(models.Model): text = models.CharField( max_length=100, verbose_name=_("Text comes here"), - help_text=_("Text description.") + help_text=_("Text description."), ) @@ -25,8 +24,10 @@ class DjangoFilterOrderingModel(models.Model): text = models.CharField(max_length=10) class Meta: - ordering = ['-date'] + ordering = ["-date"] class CategoryItem(BaseFilterableItem): - category = models.CharField(max_length=10, choices=(("home", "Home"), ("office", "Office"))) + category = models.CharField( + max_length=10, choices=(("home", "Home"), ("office", "Office")) + ) diff --git a/tests/rest_framework/test_backends.py b/tests/rest_framework/test_backends.py index 66fecf490..c38926253 100644 --- a/tests/rest_framework/test_backends.py +++ b/tests/rest_framework/test_backends.py @@ -3,16 +3,12 @@ from django.db.models import BooleanField from django.test import TestCase -from django.test.utils import override_settings +from django.test.utils import ignore_warnings, override_settings from rest_framework import generics, serializers from rest_framework.test import APIRequestFactory -from django_filters import compat, filters -from django_filters.rest_framework import ( - DjangoFilterBackend, - FilterSet, - backends -) +from django_filters import RemovedInDjangoFilter25Warning, compat, filters +from django_filters.rest_framework import DjangoFilterBackend, FilterSet, backends from ..models import Article from .models import CategoryItem, FilterableItem @@ -23,24 +19,24 @@ class FilterableItemSerializer(serializers.ModelSerializer): class Meta: model = FilterableItem - fields = '__all__' + fields = "__all__" class CategoryItemSerializer(serializers.ModelSerializer): class Meta: model = CategoryItem - fields = '__all__' + fields = "__all__" # These class are used to test a filter class. class SeveralFieldsFilter(FilterSet): - text = filters.CharFilter(lookup_expr='icontains') - decimal = filters.NumberFilter(lookup_expr='lt') - date = filters.DateFilter(lookup_expr='gt') + text = filters.CharFilter(lookup_expr="icontains") + decimal = filters.NumberFilter(lookup_expr="lt") + date = filters.DateFilter(lookup_expr="gt") class Meta: model = FilterableItem - fields = ['text', 'decimal', 'date'] + fields = ["text", "decimal", "date"] # Basic filter on a list view. @@ -51,7 +47,7 @@ class FilterableItemView(generics.ListCreateAPIView): class FilterFieldsRootView(FilterableItemView): - filterset_fields = ['decimal', 'date'] + filterset_fields = ["decimal", "date"] class FilterClassRootView(FilterableItemView): @@ -66,12 +62,11 @@ class CategoryItemView(generics.ListCreateAPIView): class GetFilterClassTests(TestCase): - def test_filterset_class(self): class Filter(FilterSet): class Meta: model = FilterableItem - fields = '__all__' + fields = "__all__" backend = DjangoFilterBackend() view = FilterableItemView() @@ -97,7 +92,7 @@ def test_filterset_class_no_queryset(self): class Filter(FilterSet): class Meta: model = FilterableItem - fields = '__all__' + fields = "__all__" backend = DjangoFilterBackend() view = FilterableItemView() @@ -109,7 +104,7 @@ class Meta: def test_filterset_fields(self): backend = DjangoFilterBackend() view = FilterableItemView() - view.filterset_fields = ['text', 'decimal', 'date'] + view.filterset_fields = ["text", "decimal", "date"] queryset = FilterableItem.objects.all() filterset_class = backend.get_filterset_class(view, queryset) @@ -118,7 +113,7 @@ def test_filterset_fields(self): def test_filterset_fields_malformed(self): backend = DjangoFilterBackend() view = FilterableItemView() - view.filterset_fields = ['non_existent'] + view.filterset_fields = ["non_existent"] queryset = FilterableItem.objects.all() msg = "'Meta.fields' must not contain non-model field names: non_existent" @@ -128,28 +123,29 @@ def test_filterset_fields_malformed(self): def test_filterset_fields_no_queryset(self): backend = DjangoFilterBackend() view = FilterableItemView() - view.filterset_fields = ['text', 'decimal', 'date'] + view.filterset_fields = ["text", "decimal", "date"] filterset_class = backend.get_filterset_class(view, None) self.assertIsNone(filterset_class) -@skipIf(compat.coreapi is None, 'coreapi must be installed') +@skipIf(compat.coreapi is None, "coreapi must be installed") class GetSchemaFieldsTests(TestCase): def test_fields_with_filterset_fields_list(self): backend = DjangoFilterBackend() fields = backend.get_schema_fields(FilterFieldsRootView()) fields = [f.name for f in fields] - self.assertEqual(fields, ['decimal', 'date']) + self.assertEqual(fields, ["decimal", "date"]) def test_filterset_fields_list_with_bad_get_queryset(self): """ See: * https://github.com/carltongibson/django-filter/issues/551 """ + class BadGetQuerySetView(FilterFieldsRootView): - filterset_fields = ['decimal', 'date'] + filterset_fields = ["decimal", "date"] def get_queryset(self): raise AttributeError("I don't have that") @@ -160,16 +156,20 @@ def get_queryset(self): warnings.simplefilter("always") fields = backend.get_schema_fields(BadGetQuerySetView()) - self.assertEqual(fields, [], "get_schema_fields should handle AttributeError") + self.assertEqual( + fields, [], "get_schema_fields should handle AttributeError" + ) - warning = "{} is not compatible with schema generation".format(BadGetQuerySetView) + warning = "{} is not compatible with schema generation".format( + BadGetQuerySetView + ) self.assertEqual(len(w), 1) self.assertEqual(str(w[0].message), warning) def test_malformed_filterset_fields(self): # Malformed filter fields should raise an exception class View(FilterFieldsRootView): - filterset_fields = ['non_existent'] + filterset_fields = ["non_existent"] backend = DjangoFilterBackend() @@ -180,14 +180,14 @@ class View(FilterFieldsRootView): def test_fields_with_filterset_fields_dict(self): class DictFilterFieldsRootView(FilterFieldsRootView): filterset_fields = { - 'decimal': ['exact', 'lt', 'gt'], + "decimal": ["exact", "lt", "gt"], } backend = DjangoFilterBackend() fields = backend.get_schema_fields(DictFilterFieldsRootView()) fields = [f.name for f in fields] - self.assertEqual(fields, ['decimal', 'decimal__lt', 'decimal__gt']) + self.assertEqual(fields, ["decimal", "decimal__lt", "decimal__gt"]) def test_fields_with_filterset_class(self): backend = DjangoFilterBackend() @@ -195,7 +195,7 @@ def test_fields_with_filterset_class(self): schemas = [f.schema for f in fields] fields = [f.name for f in fields] - self.assertEqual(fields, ['text', 'decimal', 'date']) + self.assertEqual(fields, ["text", "decimal", "date"]) self.assertIsInstance(schemas[0], compat.coreschema.String) self.assertIsInstance(schemas[1], compat.coreschema.Number) self.assertIsInstance(schemas[2], compat.coreschema.String) @@ -205,7 +205,7 @@ class RequiredFieldsFilter(SeveralFieldsFilter): required_text = filters.CharFilter(required=True) class Meta(SeveralFieldsFilter.Meta): - fields = SeveralFieldsFilter.Meta.fields + ['required_text'] + fields = SeveralFieldsFilter.Meta.fields + ["required_text"] class FilterClassWithRequiredFieldsView(FilterClassRootView): filterset_class = RequiredFieldsFilter @@ -215,7 +215,7 @@ class FilterClassWithRequiredFieldsView(FilterClassRootView): required = [f.required for f in fields] fields = [f.name for f in fields] - self.assertEqual(fields, ['text', 'decimal', 'date', 'required_text']) + self.assertEqual(fields, ["text", "decimal", "date", "required_text"]) self.assertFalse(required[0]) self.assertFalse(required[1]) self.assertFalse(required[2]) @@ -225,7 +225,9 @@ def tests_field_with_request_callable(self): def qs(request): # users expect a valid request object to be provided which cannot # be guaranteed during schema generation. - self.fail("callable queryset should not be invoked during schema generation") + self.fail( + "callable queryset should not be invoked during schema generation" + ) class F(SeveralFieldsFilter): f = filters.ModelChoiceFilter(queryset=qs) @@ -234,41 +236,47 @@ class View(FilterClassRootView): filterset_class = F view = View() - view.request = factory.get('/') + view.request = factory.get("/") backend = DjangoFilterBackend() fields = backend.get_schema_fields(view) fields = [f.name for f in fields] - self.assertEqual(fields, ['text', 'decimal', 'date', 'f']) + self.assertEqual(fields, ["text", "decimal", "date", "f"]) class GetSchemaOperationParametersTests(TestCase): + @ignore_warnings(category=RemovedInDjangoFilter25Warning) def test_get_operation_parameters_with_filterset_fields_list(self): backend = DjangoFilterBackend() fields = backend.get_schema_operation_parameters(FilterFieldsRootView()) - fields = [f['name'] for f in fields] + fields = [f["name"] for f in fields] - self.assertEqual(fields, ['decimal', 'date']) + self.assertEqual(fields, ["decimal", "date"]) + @ignore_warnings(category=RemovedInDjangoFilter25Warning) def test_get_operation_parameters_with_filterset_fields_list_with_choices(self): backend = DjangoFilterBackend() fields = backend.get_schema_operation_parameters(CategoryItemView()) self.assertEqual( fields, - [{ - 'name': 'category', - 'required': False, - 'in': 'query', - 'description': 'category', - 'schema': { - 'type': 'string', - 'enum': ['home', 'office'] - }, - - }] + [ + { + "name": "category", + "required": False, + "in": "query", + "description": "category", + "schema": {"type": "string", "enum": ["home", "office"]}, + } + ], ) + def test_deprecation_warning(self): + backend = DjangoFilterBackend() + msg = "Built-in schema generation is deprecated. Use drf-spectacular." + with self.assertWarnsMessage(RemovedInDjangoFilter25Warning, msg): + backend.get_schema_operation_parameters(FilterFieldsRootView()) + class TemplateTests(TestCase): def test_backend_output(self): @@ -277,10 +285,12 @@ def test_backend_output(self): """ view = FilterFieldsRootView() backend = view.filter_backends[0] - request = view.initialize_request(factory.get('/')) + request = view.initialize_request(factory.get("/")) html = backend().to_html(request, view.get_queryset(), view) - self.assertHTMLEqual(html, """ + self.assertHTMLEqual( + html, + """

Field filters

@@ -293,15 +303,16 @@ def test_backend_output(self):

- """) + """, + ) def test_template_path(self): view = FilterFieldsRootView() class Backend(view.filter_backends[0]): - template = 'filter_template.html' + template = "filter_template.html" - request = view.initialize_request(factory.get('/')) + request = view.initialize_request(factory.get("/")) html = Backend().to_html(request, view.get_queryset(), view) self.assertHTMLEqual(html, "Test") @@ -319,8 +330,15 @@ def test_DTL_missing(self): def test_multiple_engines(self): # See: https://github.com/carltongibson/django-filter/issues/578 - DTL = {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True} - ALT = {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, 'NAME': 'alt'} + DTL = { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + } + ALT = { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "NAME": "alt", + } # multiple DTL backends with override_settings(TEMPLATES=[DTL, ALT]): @@ -350,16 +368,15 @@ class Backend(DjangoFilterBackend): class ValidationErrorTests(TestCase): - def test_errors(self): class F(FilterSet): class Meta: model = Article - fields = ['id', 'author', 'name'] + fields = ["id", "author", "name"] view = FilterFieldsRootView() backend = DjangoFilterBackend() - request = factory.get('/?id=foo&author=bar&name=baz') + request = factory.get("/?id=foo&author=bar&name=baz") request = view.initialize_request(request) queryset = Article.objects.all() view.filterset_class = F @@ -368,81 +385,18 @@ class Meta: backend.filter_queryset(request, queryset, view) # test output, does not include error code - self.assertDictEqual(exc.exception.detail, { - 'id': ['Enter a number.'], - 'author': ['Select a valid choice. That choice is not one of the available choices.'], - }) - - -class RenamedBackendAttributesTests(TestCase): - def test_get_filter_class(self): - expected = "`Backend.get_filter_class` method should be renamed `get_filterset_class`. " \ - "See: https://django-filter.readthedocs.io/en/master/guide/migration.html" - with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') - - class Backend(DjangoFilterBackend): - def get_filter_class(self): - pass - - message = str(recorded.pop().message) - self.assertEqual(message, expected) - self.assertEqual(len(recorded), 0) - - def test_default_filter_set(self): - expected = "`Backend.default_filter_set` attribute should be renamed `filterset_base`. " \ - "See: https://django-filter.readthedocs.io/en/master/guide/migration.html" - with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') - - class Backend(DjangoFilterBackend): - default_filter_set = None - - message = str(recorded.pop().message) - self.assertEqual(message, expected) - self.assertEqual(len(recorded), 0) - - -class RenamedViewSetAttributesTests(TestCase): - - def test_filter_class(self): - expected = "`View.filter_class` attribute should be renamed `filterset_class`. " \ - "See: https://django-filter.readthedocs.io/en/master/guide/migration.html" - with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') - - class View(generics.ListCreateAPIView): - filter_class = None - - view = View() - backend = DjangoFilterBackend() - backend.get_filterset_class(view, None) - - message = str(recorded.pop().message) - self.assertEqual(message, expected) - self.assertEqual(len(recorded), 0) - - def test_filter_fields(self): - expected = "`View.filter_fields` attribute should be renamed `filterset_fields`. " \ - "See: https://django-filter.readthedocs.io/en/master/guide/migration.html" - with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') - - class View(generics.ListCreateAPIView): - filter_fields = None - - view = View() - backend = DjangoFilterBackend() - # import pdb; pdb.set_trace() - backend.get_filterset_class(view, None) - - message = str(recorded.pop().message) - self.assertEqual(message, expected) - self.assertEqual(len(recorded), 0) + self.assertDictEqual( + exc.exception.detail, + { + "id": ["Enter a number."], + "author": [ + "Select a valid choice. That choice is not one of the available choices." + ], + }, + ) class DjangoFilterBackendTestCase(TestCase): - @classmethod def setUpTestData(cls): cls.backend = DjangoFilterBackend() @@ -461,13 +415,16 @@ def test_to_html_none_filter_class(self): html = self.backend.to_html(mock.Mock(), mock.Mock(), mock.Mock()) self.assertIsNone(html) + @ignore_warnings(category=RemovedInDjangoFilter25Warning) def test_get_schema_operation_parameters_userwarning(self): with self.assertWarns(UserWarning): view = mock.Mock() - view.__class__.return_value = 'Test' + view.__class__.return_value = "Test" view.get_queryset.side_effect = Exception self.backend.get_schema_operation_parameters(view) - @mock.patch('django_filters.compat.is_crispy', return_value=True) + @mock.patch("django_filters.compat.is_crispy", return_value=True) def test_template_crispy(self, _): - self.assertEqual(self.backend.template, 'django_filters/rest_framework/crispy_form.html') + self.assertEqual( + self.backend.template, "django_filters/rest_framework/crispy_form.html" + ) diff --git a/tests/rest_framework/test_filters.py b/tests/rest_framework/test_filters.py index 8f454079c..eb904f6b3 100644 --- a/tests/rest_framework/test_filters.py +++ b/tests/rest_framework/test_filters.py @@ -8,31 +8,28 @@ class ModuleImportTests(TestCase): def is_filter(self, name, value): - return ( - isinstance(value, type) and issubclass(value, filters.Filter) - ) + return isinstance(value, type) and issubclass(value, filters.Filter) def test_imports(self): # msg = "Expected `filters.%s` to be imported in `filters.__all__`" filter_classes = [ - key for key, value - in inspect.getmembers(filters) + key + for key, value in inspect.getmembers(filters) if isinstance(value, type) and issubclass(value, filters.Filter) ] # sanity check - self.assertIn('Filter', filter_classes) - self.assertIn('BooleanFilter', filter_classes) + self.assertIn("Filter", filter_classes) + self.assertIn("BooleanFilter", filter_classes) for f in filter_classes: self.assertIn(f, filters.__all__) class BooleanFilterTests(TestCase): - def test_widget(self): # Ensure that `BooleanFilter` uses the correct widget when importing # from `rest_framework.filters`. f = filters.BooleanFilter() - self.assertEqual(f.extra['widget'], BooleanWidget) + self.assertEqual(f.extra["widget"], BooleanWidget) diff --git a/tests/rest_framework/test_filterset.py b/tests/rest_framework/test_filterset.py index f8b5aaeb1..b6f89cff5 100644 --- a/tests/rest_framework/test_filterset.py +++ b/tests/rest_framework/test_filterset.py @@ -4,7 +4,7 @@ from django.test import TestCase from django.test.utils import override_settings -from django_filters.compat import is_crispy +from django_filters.compat import crispy_forms from django_filters.rest_framework import FilterSet, filters from django_filters.widgets import BooleanWidget @@ -14,38 +14,36 @@ class ArticleFilter(FilterSet): class Meta: model = Article - fields = ['author'] + fields = ["author"] class FilterSetFilterForFieldTests(TestCase): - def test_isodatetimefilter(self): - field = Article._meta.get_field('published') - result = FilterSet.filter_for_field(field, 'published') + field = Article._meta.get_field("published") + result = FilterSet.filter_for_field(field, "published") self.assertIsInstance(result, filters.IsoDateTimeFilter) - self.assertEqual(result.field_name, 'published') + self.assertEqual(result.field_name, "published") def test_booleanfilter_widget(self): - field = User._meta.get_field('is_active') - result = FilterSet.filter_for_field(field, 'is_active') + field = User._meta.get_field("is_active") + result = FilterSet.filter_for_field(field, "is_active") self.assertIsInstance(result, filters.BooleanFilter) - self.assertEqual(result.extra['widget'], BooleanWidget) + self.assertEqual(result.extra["widget"], BooleanWidget) def test_booleanfilter_widget_nullbooleanfield(self): - field = User._meta.get_field('is_employed') - result = FilterSet.filter_for_field(field, 'is_employed') + field = User._meta.get_field("is_employed") + result = FilterSet.filter_for_field(field, "is_employed") self.assertIsInstance(result, filters.BooleanFilter) - self.assertEqual(result.extra['widget'], BooleanWidget) + self.assertEqual(result.extra["widget"], BooleanWidget) -@skipIf(is_crispy(), 'django_crispy_forms must be installed') -@override_settings(INSTALLED_APPS=settings.INSTALLED_APPS + ('crispy_forms', )) +@skipIf(crispy_forms is None, "django_crispy_forms must be installed") +@override_settings(INSTALLED_APPS=settings.INSTALLED_APPS + ("crispy_forms",)) class CrispyFormsCompatTests(TestCase): - def test_crispy_helper(self): # ensure the helper is present on the form - self.assertTrue(hasattr(ArticleFilter().form, 'helper')) + self.assertTrue(hasattr(ArticleFilter().form, "helper")) def test_form_initialization(self): # ensure that crispy compat does not prematurely initialize the form - self.assertFalse(hasattr(ArticleFilter(), '_form')) + self.assertFalse(hasattr(ArticleFilter(), "_form")) diff --git a/tests/rest_framework/test_integration.py b/tests/rest_framework/test_integration.py index 2c37618f8..ef72447d8 100644 --- a/tests/rest_framework/test_integration.py +++ b/tests/rest_framework/test_integration.py @@ -15,7 +15,7 @@ BaseFilterableItem, BasicModel, DjangoFilterOrderingModel, - FilterableItem + FilterableItem, ) factory = APIRequestFactory() @@ -24,26 +24,26 @@ class FilterableItemSerializer(serializers.ModelSerializer): class Meta: model = FilterableItem - fields = '__all__' + fields = "__all__" # Basic filter on a list view. class FilterFieldsRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer - filterset_fields = ['decimal', 'date'] + filterset_fields = ["decimal", "date"] filter_backends = (DjangoFilterBackend,) # These class are used to test a filter class. class SeveralFieldsFilter(FilterSet): - text = filters.CharFilter(lookup_expr='icontains') - decimal = filters.NumberFilter(lookup_expr='lt') - date = filters.DateFilter(lookup_expr='gt') + text = filters.CharFilter(lookup_expr="icontains") + decimal = filters.NumberFilter(lookup_expr="lt") + date = filters.DateFilter(lookup_expr="gt") class Meta: model = FilterableItem - fields = ['text', 'decimal', 'date'] + fields = ["text", "decimal", "date"] class FilterClassRootView(generics.ListCreateAPIView): @@ -55,11 +55,11 @@ class FilterClassRootView(generics.ListCreateAPIView): # These classes are used to test a misconfigured filter class. class MisconfiguredFilter(FilterSet): - text = filters.CharFilter(lookup_expr='icontains') + text = filters.CharFilter(lookup_expr="icontains") class Meta: model = BasicModel - fields = ['text'] + fields = ["text"] class IncorrectlyConfiguredRootView(generics.ListCreateAPIView): @@ -82,7 +82,7 @@ class BaseFilterableItemFilter(FilterSet): class Meta: model = BaseFilterableItem - fields = '__all__' + fields = "__all__" class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): @@ -96,7 +96,7 @@ class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): class FilterFieldsQuerysetView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer - filterset_fields = ['decimal', 'date'] + filterset_fields = ["decimal", "date"] filter_backends = (DjangoFilterBackend,) @@ -110,21 +110,26 @@ def get_queryset(self): urlpatterns = [ - path('/', FilterClassDetailView.as_view(), name='detail-view'), - path('', FilterClassRootView.as_view(), name='root-view'), - path('get-queryset/', GetQuerysetView.as_view(), name='get-queryset-view'), + path("/", FilterClassDetailView.as_view(), name="detail-view"), + path("", FilterClassRootView.as_view(), name="root-view"), + path("get-queryset/", GetQuerysetView.as_view(), name="get-queryset-view"), ] class CommonFilteringTestCase(TestCase): def _serialize_object(self, obj): - return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()} + return { + "id": obj.id, + "text": obj.text, + "decimal": str(obj.decimal), + "date": obj.date.isoformat(), + } def setUp(self): """ Create 10 FilterableItem instances. """ - base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8)) + base_data = ("a", Decimal("0.25"), datetime.date(2012, 10, 8)) for i in range(10): text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. decimal = base_data[1] + i @@ -132,10 +137,7 @@ def setUp(self): FilterableItem(text=text, decimal=decimal, date=date).save() self.objects = FilterableItem.objects - self.data = [ - self._serialize_object(obj) - for obj in self.objects.all() - ] + self.data = [self._serialize_object(obj) for obj in self.objects.all()] class IntegrationTestFiltering(CommonFilteringTestCase): @@ -150,25 +152,29 @@ def test_get_filtered_fields_root_view(self): view = FilterFieldsRootView.as_view() # Basic test with no filter. - request = factory.get('/') + request = factory.get("/") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, self.data) # Tests that the decimal filter works. - search_decimal = Decimal('2.25') - request = factory.get('/', {'decimal': '%s' % search_decimal}) + search_decimal = Decimal("2.25") + request = factory.get("/", {"decimal": "%s" % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal] + expected_data = [ + f for f in self.data if Decimal(f["decimal"]) == search_decimal + ] self.assertEqual(response.data, expected_data) # Tests that the date filter works. search_date = datetime.date(2012, 9, 22) - request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22' + request = factory.get( + "/", {"date": "%s" % search_date} + ) # search_date str: '2012-09-22' response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if parse_date(f['date']) == search_date] + expected_data = [f for f in self.data if parse_date(f["date"]) == search_date] self.assertEqual(response.data, expected_data) def test_filter_with_queryset(self): @@ -178,11 +184,13 @@ def test_filter_with_queryset(self): view = FilterFieldsQuerysetView.as_view() # Tests that the decimal filter works. - search_decimal = Decimal('2.25') - request = factory.get('/', {'decimal': '%s' % search_decimal}) + search_decimal = Decimal("2.25") + request = factory.get("/", {"decimal": "%s" % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal] + expected_data = [ + f for f in self.data if Decimal(f["decimal"]) == search_decimal + ] self.assertEqual(response.data, expected_data) def test_filter_with_get_queryset_only(self): @@ -190,7 +198,7 @@ def test_filter_with_get_queryset_only(self): Regression test for #834. """ view = GetQuerysetView.as_view() - request = factory.get('/get-queryset/') + request = factory.get("/get-queryset/") view(request).render() # Used to raise "issubclass() arg 2 must be a class or tuple of classes" # here when neither `model' nor `queryset' was specified. @@ -203,46 +211,51 @@ def test_get_filtered_class_root_view(self): view = FilterClassRootView.as_view() # Basic test with no filter. - request = factory.get('/') + request = factory.get("/") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, self.data) # Tests that the decimal filter set with 'lt' in the filter class works. - search_decimal = Decimal('4.25') - request = factory.get('/', {'decimal': '%s' % search_decimal}) + search_decimal = Decimal("4.25") + request = factory.get("/", {"decimal": "%s" % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal] + expected_data = [f for f in self.data if Decimal(f["decimal"]) < search_decimal] self.assertEqual(response.data, expected_data) # Tests that the date filter set with 'gt' in the filter class works. search_date = datetime.date(2012, 10, 2) - request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02' + request = factory.get( + "/", {"date": "%s" % search_date} + ) # search_date str: '2012-10-02' response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if parse_date(f['date']) > search_date] + expected_data = [f for f in self.data if parse_date(f["date"]) > search_date] self.assertEqual(response.data, expected_data) # Tests that the text filter set with 'icontains' in the filter class works. - search_text = 'ff' - request = factory.get('/', {'text': '%s' % search_text}) + search_text = "ff" + request = factory.get("/", {"text": "%s" % search_text}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if search_text in f['text'].lower()] + expected_data = [f for f in self.data if search_text in f["text"].lower()] self.assertEqual(response.data, expected_data) # Tests that multiple filters works. - search_decimal = Decimal('5.25') + search_decimal = Decimal("5.25") search_date = datetime.date(2012, 10, 2) - request = factory.get('/', { - 'decimal': '%s' % (search_decimal,), - 'date': '%s' % (search_date,) - }) + request = factory.get( + "/", {"decimal": "%s" % (search_decimal,), "date": "%s" % (search_date,)} + ) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [f for f in self.data if parse_date(f['date']) > search_date and - Decimal(f['decimal']) < search_decimal] + expected_data = [ + f + for f in self.data + if parse_date(f["date"]) > search_date + and Decimal(f["decimal"]) < search_decimal + ] self.assertEqual(response.data, expected_data) def test_incorrectly_configured_filter(self): @@ -251,7 +264,7 @@ def test_incorrectly_configured_filter(self): """ view = IncorrectlyConfiguredRootView.as_view() - request = factory.get('/') + request = factory.get("/") self.assertRaises(AssertionError, view, request) def test_base_model_filter(self): @@ -260,7 +273,7 @@ def test_base_model_filter(self): """ view = BaseFilterableItemFilterRootView.as_view() - request = factory.get('/?text=aaa') + request = factory.get("/?text=aaa") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) @@ -272,7 +285,7 @@ def test_unknown_filter(self): view = FilterFieldsRootView.as_view() search_integer = 10 - request = factory.get('/', {'integer': '%s' % search_integer}) + request = factory.get("/", {"integer": "%s" % search_integer}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -281,8 +294,8 @@ def test_html_rendering(self): Make sure response renders w/ backend """ view = FilterFieldsRootView.as_view() - request = factory.get('/') - request.META['HTTP_ACCEPT'] = 'text/html' + request = factory.get("/") + request.META["HTTP_ACCEPT"] = "text/html" response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -292,17 +305,17 @@ def test_raise_validation_error(self): an internal server error. """ view = FilterFieldsRootView.as_view() - request = factory.get('/?decimal=foobar') + request = factory.get("/?decimal=foobar") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {'decimal': ['Enter a number.']}) + self.assertEqual(response.data, {"decimal": ["Enter a number."]}) def test_permissive(self): """ Permissive handling should return a partially filtered result set. """ - FilterableItem.objects.create(decimal=Decimal('1.23'), date='2017-01-01') - FilterableItem.objects.create(decimal=Decimal('1.23'), date='2016-01-01') + FilterableItem.objects.create(decimal=Decimal("1.23"), date="2017-01-01") + FilterableItem.objects.create(decimal=Decimal("1.23"), date="2016-01-01") class Backend(DjangoFilterBackend): raise_exception = False @@ -311,21 +324,21 @@ class View(FilterFieldsRootView): filter_backends = (Backend,) view = View.as_view() - request = factory.get('/?decimal=foobar&date=2017-01-01') + request = factory.get("/?decimal=foobar&date=2017-01-01") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[0]['date'], '2017-01-01') + self.assertEqual(response.data[0]["date"], "2017-01-01") self.assertEqual(len(response.data), 1) -@override_settings(ROOT_URLCONF='tests.rest_framework.test_integration') +@override_settings(ROOT_URLCONF="tests.rest_framework.test_integration") class IntegrationTestDetailFiltering(CommonFilteringTestCase): """ Integration tests for filtered detail views. """ def _get_url(self, item): - return reverse('detail-view', kwargs=dict(pk=item.pk)) + return reverse("detail-view", kwargs=dict(pk=item.pk)) def test_get_filtered_detail_view(self): """ @@ -341,33 +354,39 @@ def test_get_filtered_detail_view(self): self.assertEqual(response.data, data) # Tests that the decimal filter set that should fail. - search_decimal = Decimal('4.25') + search_decimal = Decimal("4.25") high_item = self.objects.filter(decimal__gt=search_decimal)[0] response = self.client.get( - '{url}'.format(url=self._get_url(high_item)), - {'decimal': '{param}'.format(param=search_decimal)}) + "{url}".format(url=self._get_url(high_item)), + {"decimal": "{param}".format(param=search_decimal)}, + ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # Tests that the decimal filter set that should succeed. - search_decimal = Decimal('4.25') + search_decimal = Decimal("4.25") low_item = self.objects.filter(decimal__lt=search_decimal)[0] low_item_data = self._serialize_object(low_item) response = self.client.get( - '{url}'.format(url=self._get_url(low_item)), - {'decimal': '{param}'.format(param=search_decimal)}) + "{url}".format(url=self._get_url(low_item)), + {"decimal": "{param}".format(param=search_decimal)}, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, low_item_data) # Tests that multiple filters works. - search_decimal = Decimal('5.25') + search_decimal = Decimal("5.25") search_date = datetime.date(2012, 10, 2) - valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0] + valid_item = self.objects.filter( + decimal__lt=search_decimal, date__gt=search_date + )[0] valid_item_data = self._serialize_object(valid_item) response = self.client.get( - '{url}'.format(url=self._get_url(valid_item)), { - 'decimal': '{decimal}'.format(decimal=search_decimal), - 'date': '{date}'.format(date=search_date) - }) + "{url}".format(url=self._get_url(valid_item)), + { + "decimal": "{decimal}".format(decimal=search_decimal), + "date": "{date}".format(date=search_date), + }, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, valid_item_data) @@ -375,21 +394,16 @@ def test_get_filtered_detail_view(self): class DjangoFilterOrderingSerializer(serializers.ModelSerializer): class Meta: model = DjangoFilterOrderingModel - fields = '__all__' + fields = "__all__" class DjangoFilterOrderingTests(TestCase): def setUp(self): - data = [{ - 'date': datetime.date(2012, 10, 8), - 'text': 'abc' - }, { - 'date': datetime.date(2013, 10, 8), - 'text': 'bcd' - }, { - 'date': datetime.date(2014, 10, 8), - 'text': 'cde' - }] + data = [ + {"date": datetime.date(2012, 10, 8), "text": "abc"}, + {"date": datetime.date(2013, 10, 8), "text": "bcd"}, + {"date": datetime.date(2014, 10, 8), "text": "cde"}, + ] for d in data: DjangoFilterOrderingModel.objects.create(**d) @@ -399,18 +413,18 @@ class DjangoFilterOrderingView(generics.ListAPIView): serializer_class = DjangoFilterOrderingSerializer queryset = DjangoFilterOrderingModel.objects.all() filter_backends = (DjangoFilterBackend,) - filterset_fields = ['text'] - ordering = ('-date',) + filterset_fields = ["text"] + ordering = ("-date",) view = DjangoFilterOrderingView.as_view() - request = factory.get('/') + request = factory.get("/") response = view(request) self.assertEqual( response.data, [ - {'id': 3, 'date': '2014-10-08', 'text': 'cde'}, - {'id': 2, 'date': '2013-10-08', 'text': 'bcd'}, - {'id': 1, 'date': '2012-10-08', 'text': 'abc'} - ] + {"id": 3, "date": "2014-10-08", "text": "cde"}, + {"id": 2, "date": "2013-10-08", "text": "bcd"}, + {"id": 1, "date": "2012-10-08", "text": "abc"}, + ], ) diff --git a/tests/settings.py b/tests/settings.py index f0af9c768..6ed8ddc5b 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,47 +1,51 @@ - # ensure package/conf is importable from django_filters.conf import DEFAULTS DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", }, } INSTALLED_APPS = ( - 'django.contrib.contenttypes', - 'django.contrib.staticfiles', - 'django.contrib.auth', - 'rest_framework', - 'django_filters', - 'tests.rest_framework', - 'tests', + "django.contrib.contenttypes", + "django.contrib.staticfiles", + "django.contrib.auth", + "rest_framework", + "django_filters", + "tests.rest_framework", + "tests", ) MIDDLEWARE = [] -ROOT_URLCONF = 'tests.urls' +ROOT_URLCONF = "tests.urls" USE_TZ = True -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" -SECRET_KEY = 'foobar' +SECRET_KEY = "foobar" -TEMPLATES = [{ - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, -}] +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + } +] -STATIC_URL = '/static/' +STATIC_URL = "/static/" # XMLTestRunner output -TEST_OUTPUT_DIR = '.xmlcoverage' +TEST_OUTPUT_DIR = ".xmlcoverage" # help verify that DEFAULTS is importable from conf. def FILTERS_VERBOSE_LOOKUPS(): - return DEFAULTS['VERBOSE_LOOKUPS'] + return DEFAULTS["VERBOSE_LOOKUPS"] + + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/tests/test_conf.py b/tests/test_conf.py index 3fb522285..c7fc0f592 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -6,29 +6,27 @@ class DefaultSettingsTests(TestCase): - def test_verbose_lookups(self): self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict) - self.assertIn('exact', settings.VERBOSE_LOOKUPS) + self.assertIn("exact", settings.VERBOSE_LOOKUPS) def test_default_lookup_expr(self): - self.assertEqual(settings.DEFAULT_LOOKUP_EXPR, 'exact') + self.assertEqual(settings.DEFAULT_LOOKUP_EXPR, "exact") def test_disable_help_text(self): self.assertFalse(settings.DISABLE_HELP_TEXT) def test_empty_choice_label(self): - self.assertEqual(settings.EMPTY_CHOICE_LABEL, '---------') + self.assertEqual(settings.EMPTY_CHOICE_LABEL, "---------") def test_null_choice_label(self): self.assertIsNone(settings.NULL_CHOICE_LABEL) def test_null_choice_value(self): - self.assertEqual(settings.NULL_CHOICE_VALUE, 'null') + self.assertEqual(settings.NULL_CHOICE_VALUE, "null") class OverrideSettingsTests(TestCase): - def test_attribute_override(self): self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict) @@ -43,7 +41,8 @@ def test_missing_attribute_override(self): # ensure that changed setting behaves correctly when # not originally present in the user's settings. from django.conf import settings as dj_settings - self.assertFalse(hasattr(dj_settings, 'FILTERS_DISABLE_HELP_TEXT')) + + self.assertFalse(hasattr(dj_settings, "FILTERS_DISABLE_HELP_TEXT")) # Default value self.assertFalse(settings.DISABLE_HELP_TEXT) @@ -55,27 +54,26 @@ def test_missing_attribute_override(self): self.assertFalse(settings.DISABLE_HELP_TEXT) def test_non_filters_setting(self): - self.assertFalse(hasattr(settings, 'USE_TZ')) + self.assertFalse(hasattr(settings, "USE_TZ")) with override_settings(USE_TZ=False): - self.assertFalse(hasattr(settings, 'USE_TZ')) + self.assertFalse(hasattr(settings, "USE_TZ")) - self.assertFalse(hasattr(settings, 'USE_TZ')) + self.assertFalse(hasattr(settings, "USE_TZ")) def test_non_existent_setting(self): - self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR')) - self.assertFalse(hasattr(settings, 'FOOBAR')) + self.assertFalse(hasattr(settings, "FILTERS_FOOBAR")) + self.assertFalse(hasattr(settings, "FOOBAR")) - with override_settings(FILTERS_FOOBAR='blah'): - self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR')) - self.assertFalse(hasattr(settings, 'FOOBAR')) + with override_settings(FILTERS_FOOBAR="blah"): + self.assertFalse(hasattr(settings, "FILTERS_FOOBAR")) + self.assertFalse(hasattr(settings, "FOOBAR")) - self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR')) - self.assertFalse(hasattr(settings, 'FOOBAR')) + self.assertFalse(hasattr(settings, "FILTERS_FOOBAR")) + self.assertFalse(hasattr(settings, "FOOBAR")) class IsCallableTests(TestCase): - def test_behavior(self): def func(): pass @@ -96,12 +94,11 @@ def method(self): class SettingsObjectTestCase(TestCase): - - @mock.patch('django_filters.conf.DEPRECATED_SETTINGS', ['TEST_123']) - @mock.patch.dict('django_filters.conf.DEFAULTS', {'TEST_123': True}) + @mock.patch("django_filters.conf.DEPRECATED_SETTINGS", ["TEST_123"]) + @mock.patch.dict("django_filters.conf.DEFAULTS", {"TEST_123": True}) def test_get_setting_deprecated(self): with override_settings(FILTERS_TEST_123=True): with self.assertWarns(DeprecationWarning): - settings.change_setting('FILTERS_TEST_123', True, True) - test_setting = settings.get_setting('TEST_123') + settings.change_setting("FILTERS_TEST_123", True, True) + test_setting = settings.get_setting("TEST_123") self.assertTrue(test_setting) diff --git a/tests/test_fields.py b/tests/test_fields.py index 2e1680924..d30de8946 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,6 +1,9 @@ +import datetime as dt import decimal +import unittest from datetime import datetime, time, timedelta, tzinfo +import django import pytz from django import forms from django.test import TestCase, override_settings @@ -16,34 +19,33 @@ Lookup, LookupChoiceField, RangeField, - TimeRangeField + TimeRangeField, ) from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget def to_d(float_value): - return decimal.Decimal('%.2f' % float_value) + return decimal.Decimal("%.2f" % float_value) class LookupTests(TestCase): def test_empty_attrs(self): - with self.assertRaisesMessage(ValueError, ''): + with self.assertRaisesMessage(ValueError, ""): Lookup(None, None) - with self.assertRaisesMessage(ValueError, ''): - Lookup('', '') + with self.assertRaisesMessage(ValueError, ""): + Lookup("", "") def test_empty_value(self): - with self.assertRaisesMessage(ValueError, ''): - Lookup('', 'exact') + with self.assertRaisesMessage(ValueError, ""): + Lookup("", "exact") def test_empty_lookup_expr(self): - with self.assertRaisesMessage(ValueError, ''): - Lookup('Value', '') + with self.assertRaisesMessage(ValueError, ""): + Lookup("Value", "") class RangeFieldTests(TestCase): - def test_field(self): f = RangeField() self.assertEqual(len(f.fields), 2) @@ -52,14 +54,11 @@ def test_clean(self): w = RangeWidget() f = RangeField(widget=w, required=False) - self.assertEqual( - f.clean(['12.34', '55']), - slice(to_d(12.34), to_d(55))) + self.assertEqual(f.clean(["12.34", "55"]), slice(to_d(12.34), to_d(55))) self.assertIsNone(f.clean([])) class DateRangeFieldTests(TestCase): - def test_field(self): f = DateRangeField() self.assertEqual(len(f.fields), 2) @@ -69,14 +68,15 @@ def test_clean(self): w = RangeWidget() f = DateRangeField(widget=w, required=False) self.assertEqual( - f.clean(['2015-01-01', '2015-01-10']), - slice(datetime(2015, 1, 1, 0, 0, 0), - datetime(2015, 1, 10, 23, 59, 59, 999999))) + f.clean(["2015-01-01", "2015-01-10"]), + slice( + datetime(2015, 1, 1, 0, 0, 0), datetime(2015, 1, 10, 23, 59, 59, 999999) + ), + ) self.assertIsNone(f.clean([])) class DateTimeRangeFieldTests(TestCase): - def test_field(self): f = DateTimeRangeField() self.assertEqual(len(f.fields), 2) @@ -86,13 +86,12 @@ def test_clean(self): w = RangeWidget() f = DateTimeRangeField(widget=w) self.assertEqual( - f.clean(['2015-01-01 10:30', '2015-01-10 8:45']), - slice(datetime(2015, 1, 1, 10, 30, 0), - datetime(2015, 1, 10, 8, 45, 0))) + f.clean(["2015-01-01 10:30", "2015-01-10 8:45"]), + slice(datetime(2015, 1, 1, 10, 30, 0), datetime(2015, 1, 10, 8, 45, 0)), + ) class IsoDateTimeRangeFieldTests(TestCase): - def test_field(self): f = IsoDateTimeRangeField() self.assertEqual(len(f.fields), 2) @@ -101,15 +100,16 @@ def test_clean(self): w = RangeWidget() f = IsoDateTimeRangeField(widget=w) expected = slice( - datetime(2015, 1, 1, 9, 30, 1, 123000, tzinfo=timezone.utc), - datetime(2015, 1, 10, 7, 45, 2, 345000, tzinfo=timezone.utc) + datetime(2015, 1, 1, 9, 30, 1, 123000, tzinfo=dt.timezone.utc), + datetime(2015, 1, 10, 7, 45, 2, 345000, tzinfo=dt.timezone.utc), + ) + actual = f.clean( + ["2015-01-01T10:30:01.123000+01:00", "2015-01-10T08:45:02.345000+01:00"] ) - actual = f.clean(['2015-01-01T10:30:01.123000+01:00', '2015-01-10T08:45:02.345000+01:00']) self.assertEqual(expected, actual) class TimeRangeFieldTests(TestCase): - def test_field(self): f = DateRangeField() self.assertEqual(len(f.fields), 2) @@ -119,45 +119,46 @@ def test_clean(self): f = TimeRangeField(widget=w) self.assertEqual( - f.clean(['10:15', '12:30']), - slice(time(10, 15, 0), time(12, 30, 0))) + f.clean(["10:15", "12:30"]), slice(time(10, 15, 0), time(12, 30, 0)) + ) class LookupChoiceFieldTests(TestCase): - def test_field(self): inner = forms.DecimalField() - f = LookupChoiceField(inner, [('gt', 'gt'), ('lt', 'lt')]) + f = LookupChoiceField(inner, [("gt", "gt"), ("lt", "lt")]) self.assertEqual(len(f.fields), 2) def test_clean(self): inner = forms.DecimalField() - f = LookupChoiceField(inner, [('gt', 'gt'), ('lt', 'lt')], required=False) - self.assertEqual( - f.clean(['12.34', 'lt']), - Lookup(to_d(12.34), 'lt')) - self.assertEqual( - f.clean([]), - None) + f = LookupChoiceField(inner, [("gt", "gt"), ("lt", "lt")], required=False) + self.assertEqual(f.clean(["12.34", "lt"]), Lookup(to_d(12.34), "lt")) + self.assertEqual(f.clean([]), None) - with self.assertRaisesMessage(forms.ValidationError, 'Select a lookup.'): - f.clean(['12.34', '']) + with self.assertRaisesMessage(forms.ValidationError, "Select a lookup."): + f.clean(["12.34", ""]) def test_render_used_html5(self): inner = forms.DecimalField() - f = LookupChoiceField(inner, [('gt', 'gt'), ('lt', 'lt')], empty_label=None) - self.assertHTMLEqual(f.widget.render('price', ''), """ + f = LookupChoiceField(inner, [("gt", "gt"), ("lt", "lt")], empty_label=None) + self.assertHTMLEqual( + f.widget.render("price", ""), + """ """) - self.assertHTMLEqual(f.widget.render('price', ['abc', 'lt']), """ + """, + ) + self.assertHTMLEqual( + f.widget.render("price", ["abc", "lt"]), + """ """) + """, + ) class IsoDateTimeFieldTests(TestCase): @@ -180,9 +181,10 @@ def test_datetime_zulu(self): d = self.parse_input(self.reference_str + "Z") self.assertTrue(isinstance(d, datetime)) - @override_settings(TIME_ZONE='UTC') + @unittest.skipUnless(django.VERSION < (5, 0), "pytz support removed in Django 5.0") + @override_settings(TIME_ZONE="UTC") def test_datetime_timezone_awareness(self): - utc, tokyo = pytz.timezone('UTC'), pytz.timezone('Asia/Tokyo') + utc, tokyo = pytz.timezone("UTC"), pytz.timezone("Asia/Tokyo") # by default, use the server timezone reference = utc.localize(self.reference_dt) @@ -218,35 +220,45 @@ def test_datetime_timezone_naivety(self): def test_datetime_non_iso_format(self): f = IsoDateTimeField() - parsed = f.strptime('19-07-2015T51:34:13.759', '%d-%m-%YT%S:%M:%H.%f') + parsed = f.strptime("19-07-2015T51:34:13.759", "%d-%m-%YT%S:%M:%H.%f") self.assertTrue(isinstance(parsed, datetime)) self.assertEqual(parsed, self.reference_dt) def test_datetime_wrong_format(self): with self.assertRaises(ValueError): - self.parse_input('19-07-2015T51:34:13.759') + self.parse_input("19-07-2015T51:34:13.759") class BaseCSVFieldTests(TestCase): - def setUp(self): - class DecimalCSVField(BaseCSVField, forms.DecimalField): - pass - - self.field = DecimalCSVField() + class DecimalCSVField(BaseCSVField, forms.DecimalField): + pass def test_clean(self): - self.assertEqual(self.field.clean(None), None) - self.assertEqual(self.field.clean(''), []) - self.assertEqual(self.field.clean(['1']), [1]) - self.assertEqual(self.field.clean(['1', '2']), [1, 2]) - self.assertEqual(self.field.clean(['1', '2', '3']), [1, 2, 3]) + # Filter class sets required=False by default + field = self.DecimalCSVField(required=False) + + self.assertEqual(field.clean(None), None) + self.assertEqual(field.clean(""), []) + self.assertEqual(field.clean(["1"]), [1]) + self.assertEqual(field.clean(["1", "2"]), [1, 2]) + self.assertEqual(field.clean(["1", "2", "3"]), [1, 2, 3]) def test_validation_error(self): - with self.assertRaises(forms.ValidationError): - self.field.clean(['']) + field = self.DecimalCSVField() + + msg = "Enter a number." + with self.assertRaisesMessage(forms.ValidationError, msg): + field.clean(["a", "b", "c"]) + + def test_required_error(self): + field = self.DecimalCSVField(required=True) - with self.assertRaises(forms.ValidationError): - self.field.clean(['a', 'b', 'c']) + msg = "This field is required." + with self.assertRaisesMessage(forms.ValidationError, msg): + field.clean(None) + + with self.assertRaisesMessage(forms.ValidationError, msg): + field.clean([""]) def test_derived_widget(self): with self.assertRaises(AssertionError) as excinfo: @@ -256,10 +268,10 @@ def test_derived_widget(self): self.assertIn("'BaseCSVField.widget' must be a widget class", msg) self.assertIn("RangeWidget", msg) - widget = CSVWidget(attrs={'class': 'class'}) + widget = CSVWidget(attrs={"class": "class"}) field = BaseCSVField(widget=widget) self.assertIsInstance(field.widget, CSVWidget) - self.assertEqual(field.widget.attrs, {'class': 'class'}) + self.assertEqual(field.widget.attrs, {"class": "class"}) field = BaseCSVField(widget=CSVWidget) self.assertIsInstance(field.widget, CSVWidget) @@ -270,24 +282,34 @@ def test_derived_widget(self): class BaseRangeFieldTests(TestCase): - def setUp(self): - class DecimalRangeField(BaseRangeField, forms.DecimalField): - pass - - self.field = DecimalRangeField() + class DecimalRangeField(BaseRangeField, forms.DecimalField): + pass def test_clean(self): - self.assertEqual(self.field.clean(None), None) - self.assertEqual(self.field.clean(''), []) - self.assertEqual(self.field.clean([]), []) - self.assertEqual(self.field.clean(['1', '2']), [1, 2]) + # Filter class sets required=False by default + field = self.DecimalRangeField(required=False) + + self.assertEqual(field.clean(None), None) + self.assertEqual(field.clean(""), []) + self.assertEqual(field.clean([]), []) + self.assertEqual(field.clean(["1", "2"]), [1, 2]) def test_validation_error(self): - with self.assertRaises(forms.ValidationError): - self.field.clean(['']) + field = self.DecimalRangeField() + + msg = "Range query expects two values." + with self.assertRaisesMessage(forms.ValidationError, msg): + field.clean(["1"]) + + with self.assertRaisesMessage(forms.ValidationError, msg): + field.clean(["1", "2", "3"]) + + def test_required_error(self): + field = self.DecimalRangeField(required=True) - with self.assertRaises(forms.ValidationError): - self.field.clean(['1']) + msg = "This field is required." + with self.assertRaisesMessage(forms.ValidationError, msg): + field.clean(None) - with self.assertRaises(forms.ValidationError): - self.field.clean(['1', '2', '3']) + with self.assertRaisesMessage(forms.ValidationError, msg): + field.clean([""]) diff --git a/tests/test_filtering.py b/tests/test_filtering.py index 18f2b4821..6775e7f0e 100644 --- a/tests/test_filtering.py +++ b/tests/test_filtering.py @@ -1,18 +1,21 @@ import contextlib import datetime -import mock import unittest from operator import attrgetter +from unittest import mock +import django from django import forms from django.http import QueryDict -from django.test import TestCase, override_settings +from django.test import override_settings from django.utils import timezone from django.utils.timezone import make_aware, now +from django_filters.compat import TestCase from django_filters.filters import ( AllValuesFilter, AllValuesMultipleFilter, + BaseInFilter, CharFilter, ChoiceFilter, DateFromToRangeFilter, @@ -27,9 +30,10 @@ OrderingFilter, RangeFilter, TimeRangeFilter, - TypedMultipleChoiceFilter + TypedMultipleChoiceFilter, ) from django_filters.filterset import FilterSet +from django_filters.widgets import QueryArrayWidget from .models import ( STATUS_CHOICES, @@ -44,40 +48,36 @@ Node, Profile, SpacewalkRecord, - User + User, ) from .utils import MockQuerySet class CharFilterTests(TestCase): - def test_filtering(self): - b1 = Book.objects.create( - title="Ender's Game", price='1.00', average_rating=3.0) - b2 = Book.objects.create( - title="Rainbow Six", price='1.00', average_rating=3.0) - b3 = Book.objects.create( - title="Snowcrash", price='1.00', average_rating=3.0) + b1 = Book.objects.create(title="Ender's Game", price="1.00", average_rating=3.0) + b2 = Book.objects.create(title="Rainbow Six", price="1.00", average_rating=3.0) + b3 = Book.objects.create(title="Snowcrash", price="1.00", average_rating=3.0) class F(FilterSet): class Meta: model = Book - fields = ['title'] + fields = ["title"] qs = Book.objects.all() f = F(queryset=qs) - self.assertQuerysetEqual(f.qs, [b1.pk, b2.pk, b3.pk], - lambda o: o.pk, ordered=False) - f = F({'title': 'Snowcrash'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [b3.pk], lambda o: o.pk) + self.assertQuerySetEqual( + f.qs, [b1.pk, b2.pk, b3.pk], lambda o: o.pk, ordered=False + ) + f = F({"title": "Snowcrash"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [b3.pk], lambda o: o.pk) class IntegerFilterTest(TestCase): - def test_filtering(self): default_values = { - 'in_good_standing': True, - 'friendly': False, + "in_good_standing": True, + "friendly": False, } b1 = BankAccount.objects.create(amount_saved=0, **default_values) b2 = BankAccount.objects.create(amount_saved=3, **default_values) @@ -86,55 +86,52 @@ def test_filtering(self): class F(FilterSet): class Meta: model = BankAccount - fields = ['amount_saved'] + fields = ["amount_saved"] qs = BankAccount.objects.all() f = F(queryset=qs) - self.assertQuerysetEqual(f.qs, [b1.pk, b2.pk, b3.pk], - lambda o: o.pk, ordered=False) - f = F({'amount_saved': '10'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [b3.pk], lambda o: o.pk) - f = F({'amount_saved': '0'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [b1.pk], lambda o: o.pk) + self.assertQuerySetEqual( + f.qs, [b1.pk, b2.pk, b3.pk], lambda o: o.pk, ordered=False + ) + f = F({"amount_saved": "10"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [b3.pk], lambda o: o.pk) + f = F({"amount_saved": "0"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [b1.pk], lambda o: o.pk) class BooleanFilterTests(TestCase): - def test_filtering(self): - User.objects.create(username='alex', is_active=False) - User.objects.create(username='jacob', is_active=True) - User.objects.create(username='aaron', is_active=False) + User.objects.create(username="alex", is_active=False) + User.objects.create(username="jacob", is_active=True) + User.objects.create(username="aaron", is_active=False) class F(FilterSet): class Meta: model = User - fields = ['is_active'] + fields = ["is_active"] qs = User.objects.all() # '2' and '3' are how the field expects the data from the browser - f = F({'is_active': '2'}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['jacob'], lambda o: o.username, False) + f = F({"is_active": "2"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["jacob"], lambda o: o.username, False) - f = F({'is_active': '3'}, queryset=qs) - self.assertQuerysetEqual(f.qs, - ['alex', 'aaron'], - lambda o: o.username, False) + f = F({"is_active": "3"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex", "aaron"], lambda o: o.username, False) - f = F({'is_active': '1'}, queryset=qs) - self.assertQuerysetEqual(f.qs, - ['alex', 'aaron', 'jacob'], - lambda o: o.username, False) + f = F({"is_active": "1"}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["alex", "aaron", "jacob"], lambda o: o.username, False + ) class ChoiceFilterTests(TestCase): - @classmethod def setUpTestData(cls): - User.objects.create(username='alex', status=1) - User.objects.create(username='jacob', status=2) - User.objects.create(username='aaron', status=2) - User.objects.create(username='carl', status=0) + User.objects.create(username="alex", status=1) + User.objects.create(username="jacob", status=2) + User.objects.create(username="aaron", status=2) + User.objects.create(username="carl", status=0) Article.objects.create(author_id=1, published=now()) Article.objects.create(author_id=2, published=now()) @@ -146,21 +143,20 @@ def test_filtering(self): class F(FilterSet): class Meta: model = User - fields = ['status'] + fields = ["status"] f = F() - self.assertQuerysetEqual(f.qs, - ['aaron', 'alex', 'jacob', 'carl'], - lambda o: o.username, False) - f = F({'status': '1'}) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username, False) + self.assertQuerySetEqual( + f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False + ) + f = F({"status": "1"}) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username, False) - f = F({'status': '2'}) - self.assertQuerysetEqual(f.qs, ['jacob', 'aaron'], - lambda o: o.username, False) + f = F({"status": "2"}) + self.assertQuerySetEqual(f.qs, ["jacob", "aaron"], lambda o: o.username, False) - f = F({'status': '0'}) - self.assertQuerysetEqual(f.qs, ['carl'], lambda o: o.username, False) + f = F({"status": "0"}) + self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username, False) def test_filtering_on_explicitly_defined_field(self): """ @@ -168,98 +164,96 @@ def test_filtering_on_explicitly_defined_field(self): If you explicitly declare ChoiceFilter fields you **MUST** pass `choices`. """ + class F(FilterSet): status = ChoiceFilter(choices=STATUS_CHOICES) class Meta: model = User - fields = ['status'] + fields = ["status"] f = F() - self.assertQuerysetEqual(f.qs, - ['aaron', 'alex', 'jacob', 'carl'], - lambda o: o.username, False) - f = F({'status': '1'}) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username, False) + self.assertQuerySetEqual( + f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False + ) + f = F({"status": "1"}) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username, False) - f = F({'status': '2'}) - self.assertQuerysetEqual(f.qs, ['jacob', 'aaron'], - lambda o: o.username, False) + f = F({"status": "2"}) + self.assertQuerySetEqual(f.qs, ["jacob", "aaron"], lambda o: o.username, False) - f = F({'status': '0'}) - self.assertQuerysetEqual(f.qs, ['carl'], lambda o: o.username, False) + f = F({"status": "0"}) + self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username, False) def test_filtering_on_empty_choice(self): class F(FilterSet): class Meta: model = User - fields = ['status'] + fields = ["status"] - f = F({'status': ''}) - self.assertQuerysetEqual(f.qs, - ['aaron', 'alex', 'jacob', 'carl'], - lambda o: o.username, False) + f = F({"status": ""}) + self.assertQuerySetEqual( + f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False + ) def test_filtering_on_null_choice(self): - choices = [(u.pk, str(u)) for u in User.objects.order_by('id')] + choices = [(u.pk, str(u)) for u in User.objects.order_by("id")] class F(FilterSet): author = ChoiceFilter( choices=choices, - null_value='null', - null_label='NULL', + null_value="null", + null_label="NULL", ) class Meta: model = Article - fields = ['author'] + fields = ["author"] # sanity check to make sure the filter is setup correctly - f = F({'author': '1'}) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: str(o.author), False) + f = F({"author": "1"}) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: str(o.author), False) - f = F({'author': 'null'}) - self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False) + f = F({"author": "null"}) + self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) class MultipleChoiceFilterTests(TestCase): - def test_filtering(self): - User.objects.create(username='alex', status=1) - User.objects.create(username='jacob', status=2) - User.objects.create(username='aaron', status=2) - User.objects.create(username='carl', status=0) + User.objects.create(username="alex", status=1) + User.objects.create(username="jacob", status=2) + User.objects.create(username="aaron", status=2) + User.objects.create(username="carl", status=0) class F(FilterSet): status = MultipleChoiceFilter(choices=STATUS_CHOICES) class Meta: model = User - fields = ['status'] + fields = ["status"] - qs = User.objects.all().order_by('username') + qs = User.objects.all().order_by("username") f = F(queryset=qs) - self.assertQuerysetEqual( - f.qs, ['aaron', 'jacob', 'alex', 'carl'], - lambda o: o.username, False) + self.assertQuerySetEqual( + f.qs, ["aaron", "jacob", "alex", "carl"], lambda o: o.username, False + ) - f = F({'status': ['0']}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ['carl'], lambda o: o.username) + f = F({"status": ["0"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username) - f = F({'status': ['0', '1']}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ['alex', 'carl'], lambda o: o.username) + f = F({"status": ["0", "1"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex", "carl"], lambda o: o.username) - f = F({'status': ['0', '1', '2']}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username) + f = F({"status": ["0", "1", "2"]}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["aaron", "alex", "carl", "jacob"], lambda o: o.username + ) def test_filtering_on_null_choice(self): - User.objects.create(username='alex', status=1) - User.objects.create(username='jacob', status=2) - User.objects.create(username='aaron', status=2) - User.objects.create(username='carl', status=0) + User.objects.create(username="alex", status=1) + User.objects.create(username="jacob", status=2) + User.objects.create(username="aaron", status=2) + User.objects.create(username="carl", status=0) Article.objects.create(author_id=1, published=now()) Article.objects.create(author_id=2, published=now()) @@ -267,75 +261,73 @@ def test_filtering_on_null_choice(self): Article.objects.create(author_id=4, published=now()) Article.objects.create(author_id=None, published=now()) - choices = [(u.pk, str(u)) for u in User.objects.order_by('id')] + choices = [(u.pk, str(u)) for u in User.objects.order_by("id")] class F(FilterSet): author = MultipleChoiceFilter( choices=choices, - null_value='null', - null_label='NULL', + null_value="null", + null_label="NULL", ) class Meta: model = Article - fields = ['author'] + fields = ["author"] # sanity check to make sure the filter is setup correctly - f = F({'author': ['1']}) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: str(o.author), False) + f = F({"author": ["1"]}) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: str(o.author), False) - f = F({'author': ['null']}) - self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False) + f = F({"author": ["null"]}) + self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) - f = F({'author': ['1', 'null']}) - self.assertQuerysetEqual( - f.qs, ['alex', None], - lambda o: o.author and str(o.author), - False) + f = F({"author": ["1", "null"]}) + self.assertQuerySetEqual( + f.qs, ["alex", None], lambda o: o.author and str(o.author), False + ) class TypedMultipleChoiceFilterTests(TestCase): - def test_filtering(self): - User.objects.create(username='alex', status=1) - User.objects.create(username='jacob', status=2) - User.objects.create(username='aaron', status=2) - User.objects.create(username='carl', status=0) + User.objects.create(username="alex", status=1) + User.objects.create(username="jacob", status=2) + User.objects.create(username="aaron", status=2) + User.objects.create(username="carl", status=0) class F(FilterSet): - status = TypedMultipleChoiceFilter(choices=STATUS_CHOICES, coerce=lambda x: x[0:2]) + status = TypedMultipleChoiceFilter( + choices=STATUS_CHOICES, coerce=lambda x: x[0:2] + ) class Meta: model = User - fields = ['status'] + fields = ["status"] - qs = User.objects.all().order_by('username') + qs = User.objects.all().order_by("username") f = F(queryset=qs) - self.assertQuerysetEqual( - f.qs, ['aa', 'ja', 'al', 'ca'], - lambda o: o.username[0:2], False) + self.assertQuerySetEqual( + f.qs, ["aa", "ja", "al", "ca"], lambda o: o.username[0:2], False + ) - f = F({'status': ['0']}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ['ca'], lambda o: o.username[0:2]) + f = F({"status": ["0"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["ca"], lambda o: o.username[0:2]) - f = F({'status': ['0', '1']}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ['al', 'ca'], lambda o: o.username[0:2]) + f = F({"status": ["0", "1"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["al", "ca"], lambda o: o.username[0:2]) - f = F({'status': ['0', '1', '2']}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ['aa', 'al', 'ca', 'ja'], lambda o: o.username[0:2]) + f = F({"status": ["0", "1", "2"]}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["aa", "al", "ca", "ja"], lambda o: o.username[0:2] + ) class DateFilterTests(TestCase): - def test_filtering(self): today = now().date() timestamp = now().time().replace(microsecond=0) last_week = today - datetime.timedelta(days=7) check_date = str(last_week) - u = User.objects.create(username='alex') + u = User.objects.create(username="alex") Comment.objects.create(author=u, time=timestamp, date=today) Comment.objects.create(author=u, time=timestamp, date=last_week) Comment.objects.create(author=u, time=timestamp, date=today) @@ -344,22 +336,21 @@ def test_filtering(self): class F(FilterSet): class Meta: model = Comment - fields = ['date'] + fields = ["date"] - f = F({'date': check_date}, queryset=Comment.objects.all()) + f = F({"date": check_date}, queryset=Comment.objects.all()) self.assertEqual(len(f.qs), 2) - self.assertQuerysetEqual(f.qs, [2, 4], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk, False) class TimeFilterTests(TestCase): - def test_filtering(self): today = now().date() now_time = now().time().replace(microsecond=0) - ten_min_ago = (now() - datetime.timedelta(minutes=10)) + ten_min_ago = now() - datetime.timedelta(minutes=10) fixed_time = ten_min_ago.time().replace(microsecond=0) check_time = str(fixed_time) - u = User.objects.create(username='alex') + u = User.objects.create(username="alex") Comment.objects.create(author=u, time=now_time, date=today) Comment.objects.create(author=u, time=fixed_time, date=today) Comment.objects.create(author=u, time=now_time, date=today) @@ -368,20 +359,19 @@ def test_filtering(self): class F(FilterSet): class Meta: model = Comment - fields = ['time'] + fields = ["time"] - f = F({'time': check_time}, queryset=Comment.objects.all()) + f = F({"time": check_time}, queryset=Comment.objects.all()) self.assertEqual(len(f.qs), 2) - self.assertQuerysetEqual(f.qs, [2, 4], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk, False) class DateTimeFilterTests(TestCase): - def test_filtering(self): now_dt = now() ten_min_ago = now_dt - datetime.timedelta(minutes=10) one_day_ago = now_dt - datetime.timedelta(days=1) - u = User.objects.create(username='alex') + u = User.objects.create(username="alex") Article.objects.create(author=u, published=now_dt) Article.objects.create(author=u, published=ten_min_ago) Article.objects.create(author=u, published=one_day_ago) @@ -394,20 +384,19 @@ def test_filtering(self): class F(FilterSet): class Meta: model = Article - fields = ['published'] + fields = ["published"] qs = Article.objects.all() - f = F({'published': ten_min_ago}, queryset=qs) + f = F({"published": ten_min_ago}, queryset=qs) self.assertEqual(len(f.qs), 1) - self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) # this is how it would come through a browser - f = F({'published': check_dt}, queryset=qs) + f = F({"published": check_dt}, queryset=qs) self.assertEqual( - len(f.qs), - 1, - "%s isn't matching %s when cleaned" % (check_dt, ten_min_ago)) - self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk) + len(f.qs), 1, "%s isn't matching %s when cleaned" % (check_dt, ten_min_ago) + ) + self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) class DurationFilterTests(TestCase): @@ -422,94 +411,93 @@ class DurationFilterTests(TestCase): See https://en.wikipedia.org/wiki/ISO_8601#Durations """ + def setUp(self): self.r1 = SpacewalkRecord.objects.create( astronaut="Anatoly Solovyev", - duration=datetime.timedelta(hours=82, minutes=22)) + duration=datetime.timedelta(hours=82, minutes=22), + ) self.r2 = SpacewalkRecord.objects.create( astronaut="Michael Lopez-Alegria", - duration=datetime.timedelta(hours=67, minutes=40)) + duration=datetime.timedelta(hours=67, minutes=40), + ) self.r3 = SpacewalkRecord.objects.create( - astronaut="Jerry L. Ross", - duration=datetime.timedelta(hours=58, minutes=32)) + astronaut="Jerry L. Ross", duration=datetime.timedelta(hours=58, minutes=32) + ) self.r4 = SpacewalkRecord.objects.create( astronaut="John M. Grunsfeld", - duration=datetime.timedelta(hours=58, minutes=30)) + duration=datetime.timedelta(hours=58, minutes=30), + ) self.r5 = SpacewalkRecord.objects.create( astronaut="Richard Mastracchio", - duration=datetime.timedelta(hours=53, minutes=4)) + duration=datetime.timedelta(hours=53, minutes=4), + ) def test_filtering(self): - class F(FilterSet): class Meta: model = SpacewalkRecord - fields = ['duration'] + fields = ["duration"] qs = SpacewalkRecord.objects.all() # Django style: 3 days, 10 hours, 22 minutes. - f = F({'duration': '3 10:22:00'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1], lambda x: x) + f = F({"duration": "3 10:22:00"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # ISO 8601: 3 days, 10 hours, 22 minutes. - f = F({'duration': 'P3DT10H22M'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1], lambda x: x) + f = F({"duration": "P3DT10H22M"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # Django style: 82 hours, 22 minutes. - f = F({'duration': '82:22:00'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1], lambda x: x) + f = F({"duration": "82:22:00"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # ISO 8601: 82 hours, 22 minutes. - f = F({'duration': 'PT82H22M'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r1], lambda x: x) + f = F({"duration": "PT82H22M"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) def test_filtering_with_single_lookup_expr_dictionary(self): - class F(FilterSet): class Meta: model = SpacewalkRecord - fields = {'duration': ['gt', 'gte', 'lt', 'lte']} + fields = {"duration": ["gt", "gte", "lt", "lte"]} - qs = SpacewalkRecord.objects.order_by('-duration') + qs = SpacewalkRecord.objects.order_by("-duration") - f = F({'duration__gt': 'PT58H30M'}, queryset=qs) - self.assertQuerysetEqual( - f.qs, [self.r1, self.r2, self.r3], lambda x: x) + f = F({"duration__gt": "PT58H30M"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [self.r1, self.r2, self.r3], lambda x: x) - f = F({'duration__gte': 'PT58H30M'}, queryset=qs) - self.assertQuerysetEqual( - f.qs, [self.r1, self.r2, self.r3, self.r4], lambda x: x) + f = F({"duration__gte": "PT58H30M"}, queryset=qs) + self.assertQuerySetEqual( + f.qs, [self.r1, self.r2, self.r3, self.r4], lambda x: x + ) - f = F({'duration__lt': 'PT58H30M'}, queryset=qs) - self.assertQuerysetEqual( - f.qs, [self.r5], lambda x: x) + f = F({"duration__lt": "PT58H30M"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [self.r5], lambda x: x) - f = F({'duration__lte': 'PT58H30M'}, queryset=qs) - self.assertQuerysetEqual( - f.qs, [self.r4, self.r5], lambda x: x) + f = F({"duration__lte": "PT58H30M"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [self.r4, self.r5], lambda x: x) def test_filtering_with_multiple_lookup_exprs(self): - class F(FilterSet): - min_duration = DurationFilter(field_name='duration', lookup_expr='gte') - max_duration = DurationFilter(field_name='duration', lookup_expr='lte') + min_duration = DurationFilter(field_name="duration", lookup_expr="gte") + max_duration = DurationFilter(field_name="duration", lookup_expr="lte") class Meta: model = SpacewalkRecord - fields = '__all__' + fields = "__all__" - qs = SpacewalkRecord.objects.order_by('duration') + qs = SpacewalkRecord.objects.order_by("duration") - f = F({'min_duration': 'PT55H', 'max_duration': 'PT60H'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [self.r4, self.r3], lambda x: x) + f = F({"min_duration": "PT55H", "max_duration": "PT60H"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [self.r4, self.r3], lambda x: x) class ModelChoiceFilterTests(TestCase): - def test_filtering(self): - alex = User.objects.create(username='alex') - jacob = User.objects.create(username='jacob') + alex = User.objects.create(username="alex") + jacob = User.objects.create(username="jacob") date = now().date() time = now().time() Comment.objects.create(author=jacob, time=time, date=date) @@ -519,70 +507,67 @@ def test_filtering(self): class F(FilterSet): class Meta: model = Comment - fields = ['author'] + fields = ["author"] qs = Comment.objects.all() - f = F({'author': jacob.pk}, queryset=qs) - self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False) + f = F({"author": jacob.pk}, queryset=qs) + self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) - @override_settings(FILTERS_NULL_CHOICE_LABEL='No Author') + @override_settings(FILTERS_NULL_CHOICE_LABEL="No Author") def test_filtering_null(self): Article.objects.create(published=now()) - alex = User.objects.create(username='alex') + alex = User.objects.create(username="alex") Article.objects.create(author=alex, published=now()) class F(FilterSet): class Meta: model = Article - fields = ['author', 'name'] + fields = ["author", "name"] qs = Article.objects.all() - f = F({'author': 'null'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False) + f = F({"author": "null"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) def test_callable_queryset(self): # Sanity check for callable queryset arguments. # Ensure that nothing is improperly cached - User.objects.create(username='alex') - jacob = User.objects.create(username='jacob') - aaron = User.objects.create(username='aaron') + User.objects.create(username="alex") + jacob = User.objects.create(username="jacob") + aaron = User.objects.create(username="aaron") def users(request): return User.objects.filter(pk__lt=request.user.pk) class F(FilterSet): - author = ModelChoiceFilter(field_name='author', queryset=users) + author = ModelChoiceFilter(field_name="author", queryset=users) class Meta: model = Comment - fields = ['author'] + fields = ["author"] qs = Comment.objects.all() request = mock.Mock() request.user = jacob - f = F(queryset=qs, request=request).filters['author'].field - self.assertQuerysetEqual(f.queryset, [1], lambda o: o.pk, False) + f = F(queryset=qs, request=request).filters["author"].field + self.assertQuerySetEqual(f.queryset, [1], lambda o: o.pk, False) request.user = aaron - f = F(queryset=qs, request=request).filters['author'].field - self.assertQuerysetEqual(f.queryset, [1, 2], lambda o: o.pk, False) + f = F(queryset=qs, request=request).filters["author"].field + self.assertQuerySetEqual(f.queryset, [1, 2], lambda o: o.pk, False) class ModelMultipleChoiceFilterTests(TestCase): - def setUp(self): - alex = User.objects.create(username='alex') - User.objects.create(username='jacob') - aaron = User.objects.create(username='aaron') - b1 = Book.objects.create(title="Ender's Game", price='1.00', - average_rating=3.0) - b2 = Book.objects.create(title="Rainbow Six", price='1.00', - average_rating=3.0) - b3 = Book.objects.create(title="Snowcrash", price='1.00', - average_rating=3.0) - Book.objects.create(title="Stranger in a Strage Land", price='1.00', - average_rating=3.0) + alex = User.objects.create(username="alex") + User.objects.create(username="jacob") + aaron = User.objects.create(username="aaron") + b1 = Book.objects.create(title="Ender's Game", price="1.00", average_rating=3.0) + b2 = Book.objects.create(title="Rainbow Six", price="1.00", average_rating=3.0) + b3 = Book.objects.create(title="Snowcrash", price="1.00", average_rating=3.0) + Book.objects.create( + title="Stranger in a Strage Land", price="1.00", average_rating=3.0 + ) alex.favorite_books.add(b1, b2) aaron.favorite_books.add(b1, b3) @@ -592,74 +577,74 @@ def test_filtering(self): class F(FilterSet): class Meta: model = User - fields = ['favorite_books'] + fields = ["favorite_books"] - qs = User.objects.all().order_by('username') - f = F({'favorite_books': ['1']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron', 'alex'], lambda o: o.username) + qs = User.objects.all().order_by("username") + f = F({"favorite_books": ["1"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) - f = F({'favorite_books': ['1', '3']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron', 'alex'], lambda o: o.username) + f = F({"favorite_books": ["1", "3"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) - f = F({'favorite_books': ['2']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"favorite_books": ["2"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) - f = F({'favorite_books': ['4']}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.username) + f = F({"favorite_books": ["4"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username) - @override_settings(FILTERS_NULL_CHOICE_LABEL='No Favorites') + @override_settings(FILTERS_NULL_CHOICE_LABEL="No Favorites") def test_filtering_null(self): class F(FilterSet): class Meta: model = User - fields = ['favorite_books'] + fields = ["favorite_books"] qs = User.objects.all() - f = F({'favorite_books': ['null']}, queryset=qs) + f = F({"favorite_books": ["null"]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['jacob'], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["jacob"], lambda o: o.username) def test_filtering_dictionary(self): class F(FilterSet): class Meta: model = User - fields = {'favorite_books': ['exact']} + fields = {"favorite_books": ["exact"]} - qs = User.objects.all().order_by('username') - f = F({'favorite_books': ['1']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron', 'alex'], lambda o: o.username) + qs = User.objects.all().order_by("username") + f = F({"favorite_books": ["1"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) - f = F({'favorite_books': ['1', '3']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron', 'alex'], lambda o: o.username) + f = F({"favorite_books": ["1", "3"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) - f = F({'favorite_books': ['2']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"favorite_books": ["2"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) - f = F({'favorite_books': ['4']}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.username) + f = F({"favorite_books": ["4"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username) def test_filtering_on_all_of_subset_of_choices(self): class F(FilterSet): class Meta: model = User - fields = ['favorite_books'] + fields = ["favorite_books"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # This filter has a limited number of choices. - self.filters['favorite_books'].extra.update({ - 'queryset': Book.objects.filter(id__in=[1, 2]) - }) + self.filters["favorite_books"].extra.update( + {"queryset": Book.objects.filter(id__in=[1, 2])} + ) - self.filters['favorite_books'].extra['required'] = True + self.filters["favorite_books"].extra["required"] = True - qs = User.objects.all().order_by('username') + qs = User.objects.all().order_by("username") # Select all the given choices. - f = F({'favorite_books': ['1', '2']}, queryset=qs) + f = F({"favorite_books": ["1", "2"]}, queryset=qs) # The results should only include matching users - not Jacob. - self.assertQuerysetEqual(f.qs, ['aaron', 'alex'], lambda o: o.username) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) def test_filtering_on_non_required_fields(self): # See issue #132 - filtering with all options on a non-required @@ -669,7 +654,7 @@ class F(FilterSet): class Meta: model = Article - fields = ['author'] + fields = ["author"] published = now() Article.objects.create(published=published, author=self.alex) @@ -679,11 +664,8 @@ class Meta: qs = Article.objects.all() # Select all authors. - authors = [ - str(user.id) - for user in User.objects.all() - ] - f = F({'author': authors}, queryset=qs) + authors = [str(user.id) for user in User.objects.all()] + f = F({"author": authors}, queryset=qs) # The results should not include anonymous articles self.assertEqual( @@ -693,38 +675,40 @@ class Meta: class NumberFilterTests(TestCase): - def setUp(self): - Book.objects.create(title="Ender's Game", price='10.0', - average_rating=4.7999999999999998) - Book.objects.create(title="Rainbow Six", price='15.0', - average_rating=4.5999999999999996) - Book.objects.create(title="Snowcrash", price='20.0', - average_rating=4.2999999999999998) + Book.objects.create( + title="Ender's Game", price="10.0", average_rating=4.7999999999999998 + ) + Book.objects.create( + title="Rainbow Six", price="15.0", average_rating=4.5999999999999996 + ) + Book.objects.create( + title="Snowcrash", price="20.0", average_rating=4.2999999999999998 + ) def test_filtering(self): class F(FilterSet): class Meta: model = Book - fields = ['price'] + fields = ["price"] - f = F({'price': 10}, queryset=Book.objects.all()) - self.assertQuerysetEqual(f.qs, ['Ender\'s Game'], lambda o: o.title) + f = F({"price": 10}, queryset=Book.objects.all()) + self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) class RangeFilterTests(TestCase): - def setUp(self): - Book.objects.create(title="Ender's Game", price='10.0', - average_rating=4.7999999999999998) - Book.objects.create(title="Rainbow Six", price='15.0', - average_rating=4.5999999999999996) - Book.objects.create(title="Snowcrash", price='20.0', - average_rating=4.2999999999999998) - Book.objects.create(title="Refund", price='-10.0', - average_rating=5.0) - Book.objects.create(title="Free Book", price='0.0', - average_rating=0.0) + Book.objects.create( + title="Ender's Game", price="10.0", average_rating=4.7999999999999998 + ) + Book.objects.create( + title="Rainbow Six", price="15.0", average_rating=4.5999999999999996 + ) + Book.objects.create( + title="Snowcrash", price="20.0", average_rating=4.2999999999999998 + ) + Book.objects.create(title="Refund", price="-10.0", average_rating=5.0) + Book.objects.create(title="Free Book", price="0.0", average_rating=0.0) def test_filtering(self): class F(FilterSet): @@ -732,49 +716,44 @@ class F(FilterSet): class Meta: model = Book - fields = ['price'] + fields = ["price"] - qs = Book.objects.all().order_by('title') + qs = Book.objects.all().order_by("title") f = F(queryset=qs) - self.assertQuerysetEqual(f.qs, - ['Ender\'s Game', 'Free Book', 'Rainbow Six', 'Refund', 'Snowcrash'], - lambda o: o.title) - f = F({'price_min': '5', 'price_max': '15'}, queryset=qs) - self.assertQuerysetEqual(f.qs, - ['Ender\'s Game', 'Rainbow Six'], - lambda o: o.title) - - f = F({'price_min': '11'}, queryset=qs) - self.assertQuerysetEqual(f.qs, - ['Rainbow Six', 'Snowcrash'], - lambda o: o.title) - f = F({'price_max': '19'}, queryset=qs) - self.assertQuerysetEqual(f.qs, - ['Ender\'s Game', 'Free Book', 'Rainbow Six', 'Refund'], - lambda o: o.title) - - f = F({'price_min': '0', 'price_max': '12'}, queryset=qs) - self.assertQuerysetEqual(f.qs, - ['Ender\'s Game', 'Free Book'], - lambda o: o.title) - f = F({'price_min': '-11', 'price_max': '0'}, queryset=qs) - self.assertQuerysetEqual(f.qs, - ['Free Book', 'Refund'], - lambda o: o.title) - f = F({'price_min': '0', 'price_max': '0'}, queryset=qs) - self.assertQuerysetEqual(f.qs, - ['Free Book'], - lambda o: o.title) + self.assertQuerySetEqual( + f.qs, + ["Ender's Game", "Free Book", "Rainbow Six", "Refund", "Snowcrash"], + lambda o: o.title, + ) + f = F({"price_min": "5", "price_max": "15"}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title + ) + f = F({"price_min": "11"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["Rainbow Six", "Snowcrash"], lambda o: o.title) + f = F({"price_max": "19"}, queryset=qs) + self.assertQuerySetEqual( + f.qs, + ["Ender's Game", "Free Book", "Rainbow Six", "Refund"], + lambda o: o.title, + ) -class DateRangeFilterTests(TestCase): + f = F({"price_min": "0", "price_max": "12"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["Ender's Game", "Free Book"], lambda o: o.title) + f = F({"price_min": "-11", "price_max": "0"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["Free Book", "Refund"], lambda o: o.title) + f = F({"price_min": "0", "price_max": "0"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["Free Book"], lambda o: o.title) + +class DateRangeFilterTests(TestCase): class CommentFilter(FilterSet): date = DateRangeFilter() class Meta: model = Comment - fields = ['date'] + fields = ["date"] @contextlib.contextmanager def relative_to(self, today): @@ -785,7 +764,7 @@ def relative_to(self, today): two_months_ago = today - datetime.timedelta(days=62) two_years_ago = today - datetime.timedelta(days=800) - alex = User.objects.create(username='alex') + alex = User.objects.create(username="alex") time = now().time() Comment.objects.create(date=two_weeks_ago, author=alex, time=time) Comment.objects.create(date=two_years_ago, author=alex, time=time) @@ -794,315 +773,364 @@ def relative_to(self, today): Comment.objects.create(date=yesterday, author=alex, time=time) Comment.objects.create(date=two_months_ago, author=alex, time=time) - with mock.patch('django_filters.filters.now') as mock_now: + with mock.patch("django_filters.filters.now") as mock_now: mock_now.return_value = today yield def test_filtering_for_year(self): - f = self.CommentFilter({'date': 'year'}) + f = self.CommentFilter({"date": "year"}) with self.relative_to(datetime.datetime(now().year, 4, 1)): - self.assertQuerysetEqual(f.qs, [1, 3, 4, 5, 6], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3, 4, 5, 6], lambda o: o.pk, False) def test_filtering_for_month(self): - f = self.CommentFilter({'date': 'month'}) + f = self.CommentFilter({"date": "month"}) with self.relative_to(datetime.datetime(now().year, 4, 21)): - self.assertQuerysetEqual(f.qs, [1, 3, 4, 5], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3, 4, 5], lambda o: o.pk, False) def test_filtering_for_week(self): - f = self.CommentFilter({'date': 'week'}) + f = self.CommentFilter({"date": "week"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): - self.assertQuerysetEqual(f.qs, [3, 4, 5], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [3, 4, 5], lambda o: o.pk, False) def test_filtering_for_yesterday(self): - f = self.CommentFilter({'date': 'yesterday'}) + f = self.CommentFilter({"date": "yesterday"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): - self.assertQuerysetEqual(f.qs, [5], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [5], lambda o: o.pk, False) def test_filtering_for_today(self): - f = self.CommentFilter({'date': 'today'}) + f = self.CommentFilter({"date": "today"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): - self.assertQuerysetEqual(f.qs, [4], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [4], lambda o: o.pk, False) class DateFromToRangeFilterTests(TestCase): - def test_filtering(self): - adam = User.objects.create(username='adam') - kwargs = {'text': 'test', 'author': adam, 'time': '10:00'} + adam = User.objects.create(username="adam") + kwargs = {"text": "test", "author": adam, "time": "10:00"} Comment.objects.create(date=datetime.date(2016, 1, 1), **kwargs) Comment.objects.create(date=datetime.date(2016, 1, 2), **kwargs) Comment.objects.create(date=datetime.date(2016, 1, 3), **kwargs) Comment.objects.create(date=datetime.date(2016, 1, 3), **kwargs) class F(FilterSet): - published = DateFromToRangeFilter(field_name='date') + published = DateFromToRangeFilter(field_name="date") class Meta: model = Comment - fields = ['date'] + fields = ["date"] - results = F(data={ - 'published_after': '2016-01-02', - 'published_before': '2016-01-03'}) + results = F( + data={"published_after": "2016-01-02", "published_before": "2016-01-03"} + ) self.assertEqual(len(results.qs), 3) def test_filtering_ignores_time(self): tz = timezone.get_current_timezone() Article.objects.create( - published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz)) + published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz)) + published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz)) + published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz)) + published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz) + ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article - fields = ['published'] + fields = ["published"] - results = F(data={ - 'published_after': '2016-01-02', - 'published_before': '2016-01-03'}) + results = F( + data={"published_after": "2016-01-02", "published_before": "2016-01-03"} + ) self.assertEqual(len(results.qs), 3) - @override_settings(TIME_ZONE='America/Sao_Paulo') + @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") + @override_settings(TIME_ZONE="America/Sao_Paulo") def test_filtering_dst_start_midnight(self): tz = timezone.get_default_timezone() - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 14, 23, 59))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 15, 0, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 15, 1, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 16, 0, 0))) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 14, 23, 59), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 15, 0, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 15, 1, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 16, 0, 0), tz, False) + ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article - fields = ['published'] + fields = ["published"] - results = F(data={ - 'published_after': '2017-10-15', - 'published_before': '2017-10-15'}) + results = F( + data={"published_after": "2017-10-15", "published_before": "2017-10-15"} + ) self.assertEqual(len(results.qs), 2) - @override_settings(TIME_ZONE='America/Sao_Paulo') + @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") + @override_settings(TIME_ZONE="America/Sao_Paulo") def test_filtering_dst_ends_midnight(self): tz = timezone.get_default_timezone() - Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 19, 0, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 18, 23, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 18, 0, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 17, 15, 0))) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 2, 19, 0, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 2, 18, 23, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 2, 18, 0, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 2, 17, 15, 0), tz, False) + ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article - fields = ['published'] + fields = ["published"] - results = F(data={ - 'published_after': '2017-02-18', - 'published_before': '2017-02-18'}) + results = F( + data={"published_after": "2017-02-18", "published_before": "2017-02-18"} + ) self.assertEqual(len(results.qs), 2) - @override_settings(TIME_ZONE='Europe/Paris') + @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") + @override_settings(TIME_ZONE="Europe/Paris") def test_filtering_dst_start(self): tz = timezone.get_default_timezone() - Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 25, 23, 59))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 0, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 2, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 3, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 27, 0, 0))) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 3, 25, 23, 59), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 3, 26, 0, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 3, 26, 2, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 3, 26, 3, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 3, 27, 0, 0), tz, False) + ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article - fields = ['published'] + fields = ["published"] - results = F(data={ - 'published_after': '2017-3-26', - 'published_before': '2017-3-26'}) + results = F( + data={"published_after": "2017-3-26", "published_before": "2017-3-26"} + ) self.assertEqual(len(results.qs), 3) - @override_settings(TIME_ZONE='Europe/Paris') + @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") + @override_settings(TIME_ZONE="Europe/Paris") def test_filtering_dst_end(self): tz = timezone.get_default_timezone() - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 28, 23, 59))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 0, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 2, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 3, 0))) - Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 30, 0, 0))) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 28, 23, 59), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 29, 0, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 29, 2, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 29, 3, 0), tz, False) + ) + Article.objects.create( + published=make_aware(datetime.datetime(2017, 10, 30, 0, 0), tz, False) + ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article - fields = ['published'] + fields = ["published"] - results = F(data={ - 'published_after': '2017-10-29', - 'published_before': '2017-10-29'}) + results = F( + data={"published_after": "2017-10-29", "published_before": "2017-10-29"} + ) self.assertEqual(len(results.qs), 3) class DateTimeFromToRangeFilterTests(TestCase): - def test_filtering(self): tz = timezone.get_current_timezone() Article.objects.create( - published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz)) + published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz)) + published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz)) + published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz)) + published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz) + ) class F(FilterSet): published = DateTimeFromToRangeFilter() class Meta: model = Article - fields = ['published'] + fields = ["published"] - results = F(data={ - 'published_after': '2016-01-02 10:00', - 'published_before': '2016-01-03 19:00'}) + results = F( + data={ + "published_after": "2016-01-02 10:00", + "published_before": "2016-01-03 19:00", + } + ) self.assertEqual(len(results.qs), 2) -@unittest.expectedFailure class IsoDateTimeFromToRangeFilterTests(TestCase): - def test_filtering(self): tz = timezone.get_current_timezone() Article.objects.create( - published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz)) + published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz)) + published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz)) + published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz) + ) Article.objects.create( - published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz)) + published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz) + ) class F(FilterSet): published = IsoDateTimeFromToRangeFilter() class Meta: model = Article - fields = ['published'] + fields = ["published"] - dt = (datetime.datetime.now(tz=tz)) - results = F(data={ - 'published_after': '2016-01-02T10:00:00.000000' + dt.strftime("%z"), - 'published_before': '2016-01-03T19:00:00.000000' + dt.strftime("%z")}) + dt = datetime.datetime.now(tz=tz) + results = F( + data={ + "published_after": "2016-01-02T10:00:00.000000" + dt.strftime("%z"), + "published_before": "2016-01-03T19:00:00.000000" + dt.strftime("%z"), + } + ) self.assertEqual(len(results.qs), 2) class TimeRangeFilterTests(TestCase): - def test_filtering(self): - adam = User.objects.create(username='adam') - kwargs = { - 'text': 'test', 'author': adam, 'date': datetime.date.today()} - Comment.objects.create(time='7:30', **kwargs) - Comment.objects.create(time='8:00', **kwargs) - Comment.objects.create(time='9:30', **kwargs) - Comment.objects.create(time='11:00', **kwargs) + adam = User.objects.create(username="adam") + kwargs = {"text": "test", "author": adam, "date": datetime.date.today()} + Comment.objects.create(time="7:30", **kwargs) + Comment.objects.create(time="8:00", **kwargs) + Comment.objects.create(time="9:30", **kwargs) + Comment.objects.create(time="11:00", **kwargs) class F(FilterSet): time = TimeRangeFilter() class Meta: model = Comment - fields = ['time'] + fields = ["time"] - results = F(data={ - 'time_after': '8:00', - 'time_before': '10:00'}) + results = F(data={"time_after": "8:00", "time_before": "10:00"}) self.assertEqual(len(results.qs), 2) class AllValuesFilterTests(TestCase): - def test_filtering(self): - User.objects.create(username='alex') - User.objects.create(username='jacob') - User.objects.create(username='aaron') + User.objects.create(username="alex") + User.objects.create(username="jacob") + User.objects.create(username="aaron") class F(FilterSet): username = AllValuesFilter() class Meta: model = User - fields = ['username'] + fields = ["username"] self.assertEqual(list(F().qs), list(User.objects.all())) - self.assertEqual(list(F({'username': 'alex'}).qs), - [User.objects.get(username='alex')]) + self.assertEqual( + list(F({"username": "alex"}).qs), [User.objects.get(username="alex")] + ) # invalid choice - self.assertFalse(F({'username': 'jose'}).is_valid()) - self.assertEqual(list(F({'username': 'jose'}).qs), - list(User.objects.all())) + self.assertFalse(F({"username": "jose"}).is_valid()) + self.assertEqual(list(F({"username": "jose"}).qs), list(User.objects.all())) class AllValuesMultipleFilterTests(TestCase): - def test_filtering(self): - User.objects.create(username='alex') - User.objects.create(username='jacob') - User.objects.create(username='aaron') + User.objects.create(username="alex") + User.objects.create(username="jacob") + User.objects.create(username="aaron") class F(FilterSet): username = AllValuesMultipleFilter() class Meta: model = User - fields = ['username'] + fields = ["username"] self.assertEqual(list(F().qs), list(User.objects.all())) - self.assertEqual(list(F({'username': ['alex']}).qs), - [User.objects.get(username='alex')]) - self.assertEqual(list(F({'username': ['alex', 'jacob']}).qs), - list(User.objects.filter(username__in=['alex', 'jacob']))) + self.assertEqual( + list(F({"username": ["alex"]}).qs), [User.objects.get(username="alex")] + ) + self.assertEqual( + list(F({"username": ["alex", "jacob"]}).qs), + list(User.objects.filter(username__in=["alex", "jacob"])), + ) # invalid choice - self.assertFalse(F({'username': 'jose'}).is_valid()) - self.assertEqual(list(F({'username': 'jose'}).qs), - list(User.objects.all())) + self.assertFalse(F({"username": "jose"}).is_valid()) + self.assertEqual(list(F({"username": "jose"}).qs), list(User.objects.all())) class FilterMethodTests(TestCase): - def setUp(self): - User.objects.create(username='alex') - User.objects.create(username='jacob') - User.objects.create(username='aaron') + User.objects.create(username="alex") + User.objects.create(username="jacob") + User.objects.create(username="aaron") def test_filtering(self): class F(FilterSet): - username = CharFilter(method='filter_username') + username = CharFilter(method="filter_username") class Meta: model = User - fields = ['username'] + fields = ["username"] def filter_username(self, queryset, name, value): return queryset.filter(**{name: value}) self.assertEqual(list(F().qs), list(User.objects.all())) - self.assertEqual(list(F({'username': 'alex'}).qs), - [User.objects.get(username='alex')]) - self.assertEqual(list(F({'username': 'jose'}).qs), - list()) + self.assertEqual( + list(F({"username": "alex"}).qs), [User.objects.get(username="alex")] + ) + self.assertEqual(list(F({"username": "jose"}).qs), list()) def test_filtering_callable(self): def filter_username(queryset, name, value): @@ -1113,185 +1141,184 @@ class F(FilterSet): class Meta: model = User - fields = ['username'] + fields = ["username"] self.assertEqual(list(F().qs), list(User.objects.all())) - self.assertEqual(list(F({'username': 'alex'}).qs), - [User.objects.get(username='alex')]) - self.assertEqual(list(F({'username': 'jose'}).qs), - list()) + self.assertEqual( + list(F({"username": "alex"}).qs), [User.objects.get(username="alex")] + ) + self.assertEqual(list(F({"username": "jose"}).qs), list()) class O2ORelationshipTests(TestCase): - def setUp(self): a1 = Account.objects.create( - name='account1', in_good_standing=False, friendly=False) + name="account1", in_good_standing=False, friendly=False + ) a2 = Account.objects.create( - name='account2', in_good_standing=True, friendly=True) + name="account2", in_good_standing=True, friendly=True + ) a3 = Account.objects.create( - name='account3', in_good_standing=True, friendly=False) + name="account3", in_good_standing=True, friendly=False + ) a4 = Account.objects.create( - name='account4', in_good_standing=False, friendly=True) + name="account4", in_good_standing=False, friendly=True + ) Profile.objects.create(account=a1, likes_coffee=True, likes_tea=False) Profile.objects.create(account=a2, likes_coffee=False, likes_tea=True) Profile.objects.create(account=a3, likes_coffee=True, likes_tea=True) Profile.objects.create(account=a4, likes_coffee=False, likes_tea=False) def test_o2o_relation(self): - class F(FilterSet): class Meta: model = Profile - fields = ('account',) + fields = ("account",) f = F() self.assertEqual(f.qs.count(), 4) - f = F({'account': 1}) + f = F({"account": 1}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [1], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_o2o_relation_dictionary(self): - class F(FilterSet): class Meta: model = Profile - fields = {'account': ['exact'], } + fields = { + "account": ["exact"], + } f = F() self.assertEqual(f.qs.count(), 4) - f = F({'account': 1}) + f = F({"account": 1}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [1], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_reverse_o2o_relation(self): class F(FilterSet): class Meta: model = Account - fields = ('profile',) + fields = ("profile",) f = F() self.assertEqual(f.qs.count(), 4) - f = F({'profile': 1}) + f = F({"profile": 1}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [1], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_o2o_relation_attribute(self): class F(FilterSet): class Meta: model = Profile - fields = ('account__in_good_standing',) + fields = ("account__in_good_standing",) f = F() self.assertEqual(f.qs.count(), 4) - f = F({'account__in_good_standing': '2'}) + f = F({"account__in_good_standing": "2"}) self.assertEqual(f.qs.count(), 2) - self.assertQuerysetEqual(f.qs, [2, 3], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [2, 3], lambda o: o.pk, False) def test_o2o_relation_attribute2(self): class F(FilterSet): class Meta: model = Profile - fields = ('account__in_good_standing', 'account__friendly',) + fields = ( + "account__in_good_standing", + "account__friendly", + ) f = F() self.assertEqual(f.qs.count(), 4) - f = F({'account__in_good_standing': '2', 'account__friendly': '2'}) + f = F({"account__in_good_standing": "2", "account__friendly": "2"}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) def test_reverse_o2o_relation_attribute(self): class F(FilterSet): class Meta: model = Account - fields = ('profile__likes_coffee',) + fields = ("profile__likes_coffee",) f = F() self.assertEqual(f.qs.count(), 4) - f = F({'profile__likes_coffee': '2'}) + f = F({"profile__likes_coffee": "2"}) self.assertEqual(f.qs.count(), 2) - self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) def test_reverse_o2o_relation_attribute2(self): class F(FilterSet): class Meta: model = Account - fields = ('profile__likes_coffee', 'profile__likes_tea') + fields = ("profile__likes_coffee", "profile__likes_tea") f = F() self.assertEqual(f.qs.count(), 4) - f = F({'profile__likes_coffee': '2', 'profile__likes_tea': '2'}) + f = F({"profile__likes_coffee": "2", "profile__likes_tea": "2"}) self.assertEqual(f.qs.count(), 1) - self.assertQuerysetEqual(f.qs, [3], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [3], lambda o: o.pk) class FKRelationshipTests(TestCase): - def test_fk_relation(self): - company1 = Company.objects.create(name='company1') - company2 = Company.objects.create(name='company2') - Location.objects.create( - company=company1, open_days="some", zip_code="90210") - Location.objects.create( - company=company2, open_days="WEEKEND", zip_code="11111") - Location.objects.create( - company=company1, open_days="monday", zip_code="12345") + company1 = Company.objects.create(name="company1") + company2 = Company.objects.create(name="company2") + Location.objects.create(company=company1, open_days="some", zip_code="90210") + Location.objects.create(company=company2, open_days="WEEKEND", zip_code="11111") + Location.objects.create(company=company1, open_days="monday", zip_code="12345") class F(FilterSet): class Meta: model = Location - fields = ('company',) + fields = ("company",) f = F() self.assertEqual(f.qs.count(), 3) - f = F({'company': 1}) + f = F({"company": 1}) self.assertEqual(f.qs.count(), 2) - self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False) + self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) def test_reverse_fk_relation(self): - alex = User.objects.create(username='alex') - jacob = User.objects.create(username='jacob') + alex = User.objects.create(username="alex") + jacob = User.objects.create(username="jacob") date = now().date() time = now().time() - Comment.objects.create(text='comment 1', - author=jacob, time=time, date=date) - Comment.objects.create(text='comment 2', - author=alex, time=time, date=date) - Comment.objects.create(text='comment 3', - author=jacob, time=time, date=date) + Comment.objects.create(text="comment 1", author=jacob, time=time, date=date) + Comment.objects.create(text="comment 2", author=alex, time=time, date=date) + Comment.objects.create(text="comment 3", author=jacob, time=time, date=date) class F(FilterSet): class Meta: model = User - fields = ['comments'] + fields = ["comments"] qs = User.objects.all() - f = F({'comments': [2]}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"comments": [2]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) class F(FilterSet): comments = AllValuesFilter() class Meta: model = User - fields = ['comments'] + fields = ["comments"] - f = F({'comments': 2}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"comments": 2}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) def test_fk_relation_attribute(self): now_dt = now() - alex = User.objects.create(username='alex') - jacob = User.objects.create(username='jacob') - User.objects.create(username='aaron') + alex = User.objects.create(username="alex") + jacob = User.objects.create(username="jacob") + User.objects.create(username="aaron") Article.objects.create(author=alex, published=now_dt) Article.objects.create(author=jacob, published=now_dt) @@ -1300,88 +1327,79 @@ def test_fk_relation_attribute(self): class F(FilterSet): class Meta: model = Article - fields = ['author__username'] + fields = ["author__username"] - self.assertEqual(list(F.base_filters), ['author__username']) - self.assertEqual(F({'author__username': 'alex'}).qs.count(), 2) - self.assertEqual(F({'author__username': 'jacob'}).qs.count(), 1) + self.assertEqual(list(F.base_filters), ["author__username"]) + self.assertEqual(F({"author__username": "alex"}).qs.count(), 2) + self.assertEqual(F({"author__username": "jacob"}).qs.count(), 1) class F(FilterSet): author__username = AllValuesFilter() class Meta: model = Article - fields = ['author__username'] + fields = ["author__username"] - self.assertEqual(F({'author__username': 'alex'}).qs.count(), 2) + self.assertEqual(F({"author__username": "alex"}).qs.count(), 2) def test_reverse_fk_relation_attribute(self): - alex = User.objects.create(username='alex') - jacob = User.objects.create(username='jacob') + alex = User.objects.create(username="alex") + jacob = User.objects.create(username="jacob") date = now().date() time = now().time() - Comment.objects.create(text='comment 1', - author=jacob, time=time, date=date) - Comment.objects.create(text='comment 2', - author=alex, time=time, date=date) - Comment.objects.create(text='comment 3', - author=jacob, time=time, date=date) + Comment.objects.create(text="comment 1", author=jacob, time=time, date=date) + Comment.objects.create(text="comment 2", author=alex, time=time, date=date) + Comment.objects.create(text="comment 3", author=jacob, time=time, date=date) class F(FilterSet): class Meta: model = User - fields = ['comments__text'] + fields = ["comments__text"] qs = User.objects.all() - f = F({'comments__text': 'comment 2'}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"comments__text": "comment 2"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) class F(FilterSet): comments__text = AllValuesFilter() class Meta: model = User - fields = ['comments__text'] + fields = ["comments__text"] - f = F({'comments__text': 'comment 2'}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"comments__text": "comment 2"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) - @unittest.skip('todo - need correct models') + @unittest.skip("todo - need correct models") def test_fk_relation_multiple_attributes(self): pass @unittest.expectedFailure def test_reverse_fk_relation_multiple_attributes(self): - company = Company.objects.create(name='company') - Location.objects.create( - company=company, open_days="some", zip_code="90210") - Location.objects.create( - company=company, open_days="WEEKEND", zip_code="11111") + company = Company.objects.create(name="company") + Location.objects.create(company=company, open_days="some", zip_code="90210") + Location.objects.create(company=company, open_days="WEEKEND", zip_code="11111") class F(FilterSet): class Meta: model = Company - fields = ('locations__zip_code', 'locations__open_days') + fields = ("locations__zip_code", "locations__open_days") - f = F({'locations__zip_code': '90210', - 'locations__open_days': 'WEEKEND'}) + f = F({"locations__zip_code": "90210", "locations__open_days": "WEEKEND"}) self.assertEqual(f.qs.count(), 0) class M2MRelationshipTests(TestCase): - def setUp(self): - alex = User.objects.create(username='alex', status=1) - User.objects.create(username='jacob', status=1) - aaron = User.objects.create(username='aaron', status=1) - b1 = Book.objects.create(title="Ender's Game", price='1.00', - average_rating=3.0) - b2 = Book.objects.create(title="Rainbow Six", price='2.00', - average_rating=4.0) - b3 = Book.objects.create(title="Snowcrash", price='1.00', - average_rating=4.0) - Book.objects.create(title="Stranger in a Strage Land", price='2.00', - average_rating=3.0) + alex = User.objects.create(username="alex", status=1) + User.objects.create(username="jacob", status=1) + aaron = User.objects.create(username="aaron", status=1) + b1 = Book.objects.create(title="Ender's Game", price="1.00", average_rating=3.0) + b2 = Book.objects.create(title="Rainbow Six", price="2.00", average_rating=4.0) + b3 = Book.objects.create(title="Snowcrash", price="1.00", average_rating=4.0) + Book.objects.create( + title="Stranger in a Strage Land", price="2.00", average_rating=3.0 + ) alex.favorite_books.add(b1, b2) aaron.favorite_books.add(b1, b3) @@ -1389,108 +1407,125 @@ def test_m2m_relation(self): class F(FilterSet): class Meta: model = User - fields = ['favorite_books'] + fields = ["favorite_books"] - qs = User.objects.all().order_by('username') - f = F({'favorite_books': ['1']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron', 'alex'], lambda o: o.username) + qs = User.objects.all().order_by("username") + f = F({"favorite_books": ["1"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) - f = F({'favorite_books': ['1', '3']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron', 'alex'], lambda o: o.username) + f = F({"favorite_books": ["1", "3"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) - f = F({'favorite_books': ['2']}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"favorite_books": ["2"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) - f = F({'favorite_books': ['4']}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.username) + f = F({"favorite_books": ["4"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username) def test_reverse_m2m_relation(self): class F(FilterSet): class Meta: model = Book - fields = ['lovers'] + fields = ["lovers"] - qs = Book.objects.all().order_by('title') - f = F({'lovers': [1]}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title) + qs = Book.objects.all().order_by("title") + f = F({"lovers": [1]}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title + ) class F(FilterSet): lovers = AllValuesFilter() class Meta: model = Book - fields = ['lovers'] + fields = ["lovers"] - f = F({'lovers': 1}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title) + f = F({"lovers": 1}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title + ) def test_m2m_relation_attribute(self): class F(FilterSet): class Meta: model = User - fields = ['favorite_books__title'] + fields = ["favorite_books__title"] - qs = User.objects.all().order_by('username') - f = F({'favorite_books__title': "Ender's Game"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron', 'alex'], lambda o: o.username) + qs = User.objects.all().order_by("username") + f = F({"favorite_books__title": "Ender's Game"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) - f = F({'favorite_books__title': 'Rainbow Six'}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"favorite_books__title": "Rainbow Six"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) + # No choices given, so filtering does nothing. class F(FilterSet): favorite_books__title = MultipleChoiceFilter() class Meta: model = User - fields = ['favorite_books__title'] + fields = ["favorite_books__title"] f = F() - self.assertEqual( - len(f.filters['favorite_books__title'].field.choices), 0) - # f = F({'favorite_books__title': ['1', '3']}, - # queryset=qs) - # self.assertQuerysetEqual( - # f.qs, ['aaron', 'alex'], lambda o: o.username) + self.assertEqual(len(f.filters["favorite_books__title"].field.choices), 0) + + # Specifying choices allows filter to work. (See also AllValues variants.) + class F(FilterSet): + favorite_books__title = MultipleChoiceFilter( + choices=[ + (b.title, b.title) for b in Book.objects.all() + ] + ) + + class Meta: + model = User + fields = ["favorite_books__title"] + + f = F({'favorite_books__title': ["Ender's Game", "Snowcrash"]}, queryset=qs) + self.assertIs(True, f.form.is_valid(), list(f.filters["favorite_books__title"].field.choices)) + self.assertQuerySetEqual( + f.qs, ['aaron', 'alex'], + lambda o: o.username, + ) class F(FilterSet): favorite_books__title = AllValuesFilter() class Meta: model = User - fields = ['favorite_books__title'] + fields = ["favorite_books__title"] - f = F({'favorite_books__title': "Snowcrash"}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron'], lambda o: o.username) + f = F({"favorite_books__title": "Snowcrash"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["aaron"], lambda o: o.username) def test_reverse_m2m_relation_attribute(self): class F(FilterSet): class Meta: model = Book - fields = ['lovers__username'] + fields = ["lovers__username"] - qs = Book.objects.all().order_by('title') - f = F({'lovers__username': "alex"}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title) + qs = Book.objects.all().order_by("title") + f = F({"lovers__username": "alex"}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title + ) - f = F({'lovers__username': 'jacob'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.title) + f = F({"lovers__username": "jacob"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [], lambda o: o.title) class F(FilterSet): lovers__username = MultipleChoiceFilter() class Meta: model = Book - fields = ['lovers__username'] + fields = ["lovers__username"] f = F() - self.assertEqual( - len(f.filters['lovers__username'].field.choices), 0) + self.assertEqual(len(f.filters["lovers__username"].field.choices), 0) # f = F({'lovers__username': ['1', '3']}, # queryset=qs) - # self.assertQuerysetEqual( + # self.assertQuerySetEqual( # f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title) class F(FilterSet): @@ -1498,62 +1533,64 @@ class F(FilterSet): class Meta: model = Book - fields = ['lovers__username'] + fields = ["lovers__username"] - f = F({'lovers__username': "alex"}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title) + f = F({"lovers__username": "alex"}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title + ) @unittest.expectedFailure def test_m2m_relation_multiple_attributes(self): class F(FilterSet): class Meta: model = User - fields = ['favorite_books__price', - 'favorite_books__average_rating'] + fields = ["favorite_books__price", "favorite_books__average_rating"] - qs = User.objects.all().order_by('username') - f = F({'favorite_books__price': "1.00", - 'favorite_books__average_rating': 4.0}, - queryset=qs) - self.assertQuerysetEqual(f.qs, ['aaron'], lambda o: o.username) + qs = User.objects.all().order_by("username") + f = F( + {"favorite_books__price": "1.00", "favorite_books__average_rating": 4.0}, + queryset=qs, + ) + self.assertQuerySetEqual(f.qs, ["aaron"], lambda o: o.username) - f = F({'favorite_books__price': "3.00", - 'favorite_books__average_rating': 4.0}, - queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.username) + f = F( + {"favorite_books__price": "3.00", "favorite_books__average_rating": 4.0}, + queryset=qs, + ) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username) @unittest.expectedFailure def test_reverse_m2m_relation_multiple_attributes(self): class F(FilterSet): class Meta: model = Book - fields = ['lovers__status', 'lovers__username'] + fields = ["lovers__status", "lovers__username"] - qs = Book.objects.all().order_by('title') - f = F({'lovers__status': 1, 'lovers__username': "alex"}, queryset=qs) - self.assertQuerysetEqual( - f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title) + qs = Book.objects.all().order_by("title") + f = F({"lovers__status": 1, "lovers__username": "alex"}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title + ) - f = F({'lovers__status': 1, 'lovers__username': 'jacob'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.title) + f = F({"lovers__status": 1, "lovers__username": "jacob"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [], lambda o: o.title) - @unittest.skip('todo') + @unittest.skip("todo") def test_fk_relation_on_m2m_relation(self): pass - @unittest.skip('todo') + @unittest.skip("todo") def test_fk_relation_attribute_on_m2m_relation(self): pass class SymmetricalSelfReferentialRelationshipTests(TestCase): - def setUp(self): - n1 = Node.objects.create(name='one') - n2 = Node.objects.create(name='two') - n3 = Node.objects.create(name='three') - n4 = Node.objects.create(name='four') + n1 = Node.objects.create(name="one") + n2 = Node.objects.create(name="two") + n3 = Node.objects.create(name="three") + n4 = Node.objects.create(name="four") n1.adjacents.add(n2) n2.adjacents.add(n3) n2.adjacents.add(n4) @@ -1563,20 +1600,19 @@ def test_relation(self): class F(FilterSet): class Meta: model = Node - fields = ['adjacents'] + fields = ["adjacents"] - qs = Node.objects.all().order_by('pk') - f = F({'adjacents': ['1']}, queryset=qs) - self.assertQuerysetEqual(f.qs, [2, 4], lambda o: o.pk) + qs = Node.objects.all().order_by("pk") + f = F({"adjacents": ["1"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk) class NonSymmetricalSelfReferentialRelationshipTests(TestCase): - def setUp(self): - n1 = DirectedNode.objects.create(name='one') - n2 = DirectedNode.objects.create(name='two') - n3 = DirectedNode.objects.create(name='three') - n4 = DirectedNode.objects.create(name='four') + n1 = DirectedNode.objects.create(name="one") + n2 = DirectedNode.objects.create(name="two") + n3 = DirectedNode.objects.create(name="three") + n4 = DirectedNode.objects.create(name="four") n1.outbound_nodes.add(n2) n2.outbound_nodes.add(n3) n2.outbound_nodes.add(n4) @@ -1586,112 +1622,131 @@ def test_forward_relation(self): class F(FilterSet): class Meta: model = DirectedNode - fields = ['outbound_nodes'] + fields = ["outbound_nodes"] - qs = DirectedNode.objects.all().order_by('pk') - f = F({'outbound_nodes': ['1']}, queryset=qs) - self.assertQuerysetEqual(f.qs, [4], lambda o: o.pk) + qs = DirectedNode.objects.all().order_by("pk") + f = F({"outbound_nodes": ["1"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, [4], lambda o: o.pk) def test_reverse_relation(self): class F(FilterSet): class Meta: model = DirectedNode - fields = ['inbound_nodes'] + fields = ["inbound_nodes"] - qs = DirectedNode.objects.all().order_by('pk') - f = F({'inbound_nodes': ['1']}, queryset=qs) - self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk) + qs = DirectedNode.objects.all().order_by("pk") + f = F({"inbound_nodes": ["1"]}, queryset=qs) + self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) -@override_settings(TIME_ZONE='UTC') +@override_settings(TIME_ZONE="UTC") class TransformedQueryExpressionFilterTests(TestCase): - def test_filtering(self): now_dt = now() after_5pm = now_dt.replace(hour=18) before_5pm = now_dt.replace(hour=16) - u = User.objects.create(username='alex') + u = User.objects.create(username="alex") a = Article.objects.create(author=u, published=after_5pm) Article.objects.create(author=u, published=before_5pm) class F(FilterSet): class Meta: model = Article - fields = {'published': ['hour__gte']} + fields = {"published": ["hour__gte"]} qs = Article.objects.all() - f = F({'published__hour__gte': 17}, queryset=qs) + f = F({"published__hour__gte": 17}, queryset=qs) self.assertEqual(len(f.qs), 1) - self.assertQuerysetEqual(f.qs, [a.pk], lambda o: o.pk) + self.assertQuerySetEqual(f.qs, [a.pk], lambda o: o.pk) class LookupChoiceFilterTests(TestCase): - class BookFilter(FilterSet): - price = LookupChoiceFilter(lookup_choices=['lt', 'gt'], field_class=forms.DecimalField) + price = LookupChoiceFilter( + lookup_choices=["lt", "gt"], field_class=forms.DecimalField + ) class Meta: model = Book - fields = ['price'] + fields = ["price"] @classmethod def setUpTestData(cls): - Book.objects.create(title="Ender's Game", price='10.0', - average_rating=4.7999999999999998) - Book.objects.create(title="Rainbow Six", price='15.0', - average_rating=4.5999999999999996) - Book.objects.create(title="Snowcrash", price='20.0', - average_rating=4.2999999999999998) + Book.objects.create( + title="Ender's Game", price="10.0", average_rating=4.7999999999999998 + ) + Book.objects.create( + title="Rainbow Six", price="15.0", average_rating=4.5999999999999996 + ) + Book.objects.create( + title="Snowcrash", price="20.0", average_rating=4.2999999999999998 + ) def test_filtering(self): F = self.BookFilter - f = F({'price': '15', 'price_lookup': 'lt'}) - self.assertQuerysetEqual(f.qs, ['Ender\'s Game'], lambda o: o.title) - f = F({'price': '15', 'price_lookup': 'lt'}) - self.assertQuerysetEqual(f.qs, ['Ender\'s Game'], lambda o: o.title) - f = F({'price': '', 'price_lookup': 'lt'}) + f = F({"price": "15", "price_lookup": "lt"}) + self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) + f = F({"price": "15", "price_lookup": "lt"}) + self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) + f = F({"price": "", "price_lookup": "lt"}) self.assertTrue(f.is_valid()) - self.assertQuerysetEqual(f.qs, - ['Ender\'s Game', 'Rainbow Six', 'Snowcrash'], - lambda o: o.title, ordered=False) - f = F({'price': '15'}) + self.assertQuerySetEqual( + f.qs, + ["Ender's Game", "Rainbow Six", "Snowcrash"], + lambda o: o.title, + ordered=False, + ) + f = F({"price": "15"}) self.assertFalse(f.is_valid()) - self.assertQuerysetEqual(f.qs, - ['Ender\'s Game', 'Rainbow Six', 'Snowcrash'], - lambda o: o.title, ordered=False) + self.assertQuerySetEqual( + f.qs, + ["Ender's Game", "Rainbow Six", "Snowcrash"], + lambda o: o.title, + ordered=False, + ) def test_inner_field_class_validation(self): - f = self.BookFilter({'price': 'asdf', 'price_lookup': 'lt'}) + f = self.BookFilter({"price": "asdf", "price_lookup": "lt"}) self.assertFalse(f.is_valid()) - self.assertEqual(f.errors, { - 'price': ['Enter a number.'], - }) + self.assertEqual( + f.errors, + { + "price": ["Enter a number."], + }, + ) def test_lookup_choices_validation(self): - f = self.BookFilter({'price': '1', 'price_lookup': 'asdf'}) + f = self.BookFilter({"price": "1", "price_lookup": "asdf"}) self.assertFalse(f.is_valid()) - self.assertEqual(f.errors, { - 'price': ['Select a valid choice. asdf is not one of the available choices.'], - }) + self.assertEqual( + f.errors, + { + "price": [ + "Select a valid choice. asdf is not one of the available choices." + ], + }, + ) def test_lookup_omitted(self): - f = self.BookFilter({'price': '1'}) + f = self.BookFilter({"price": "1"}) self.assertFalse(f.is_valid()) - self.assertEqual(f.errors, { - 'price': ['Select a lookup.'], - }) + self.assertEqual( + f.errors, + { + "price": ["Select a lookup."], + }, + ) -@override_settings(TIME_ZONE='UTC') +@override_settings(TIME_ZONE="UTC") class CSVFilterTests(TestCase): - def setUp(self): - u1 = User.objects.create(username='alex', status=1) - u2 = User.objects.create(username='jacob', status=2) - User.objects.create(username='aaron', status=2) - User.objects.create(username='carl', status=0) + u1 = User.objects.create(username="alex", status=1) + u2 = User.objects.create(username="jacob", status=2) + User.objects.create(username="aaron", status=2) + User.objects.create(username="carl", status=0) now_dt = now() after_5pm = now_dt.replace(hour=18) @@ -1706,124 +1761,145 @@ class UserFilter(FilterSet): class Meta: model = User fields = { - 'username': ['in'], - 'status': ['in'], + "username": ["in"], + "status": ["in"], } class ArticleFilter(FilterSet): class Meta: model = Article fields = { - 'author': ['in'], - 'published': ['in'], + "author": ["in"], + "published": ["in"], } self.user_filter = UserFilter self.article_filter = ArticleFilter - self.after_5pm = after_5pm.strftime('%Y-%m-%d %H:%M:%S.%f') - self.before_5pm = before_5pm.strftime('%Y-%m-%d %H:%M:%S.%f') + self.after_5pm = after_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") + self.before_5pm = before_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") def test_numeric_filtering(self): F = self.user_filter - qs = User.objects.order_by('pk') + qs = User.objects.order_by("pk") cases = [ (None, [1, 2, 3, 4]), - (QueryDict('status__in=1&status__in=2'), [2, 3]), - ({'status__in': ''}, [1, 2, 3, 4]), - ({'status__in': ','}, []), - ({'status__in': '0'}, [4]), - ({'status__in': '0,2'}, [2, 3, 4]), - ({'status__in': '0,,1'}, [1, 4]), - ({'status__in': '2'}, [2, 3]), + (QueryDict("status__in=1&status__in=2"), [2, 3]), + ({"status__in": ""}, [1, 2, 3, 4]), + ({"status__in": ","}, []), + ({"status__in": "0"}, [4]), + ({"status__in": "0,2"}, [2, 3, 4]), + ({"status__in": "0,,1"}, [1, 4]), + ({"status__in": "2"}, [2, 3]), ] for params, expected in cases: with self.subTest(params=params, expected=expected): - self.assertQuerysetEqual(F(params, queryset=qs).qs, - expected, attrgetter('pk')) + self.assertQuerySetEqual( + F(params, queryset=qs).qs, expected, attrgetter("pk") + ) def test_string_filtering(self): F = self.user_filter - qs = User.objects.order_by('pk') + qs = User.objects.order_by("pk") cases = [ (None, [1, 2, 3, 4]), - (QueryDict('username__in=alex&username__in=aaron'), [3]), - ({'username__in': ''}, [1, 2, 3, 4]), - ({'username__in': ','}, []), - ({'username__in': 'alex'}, [1]), - ({'username__in': 'alex,aaron'}, [1, 3]), - ({'username__in': 'alex,,aaron'}, [1, 3]), - ({'username__in': 'alex,'}, [1]), + (QueryDict("username__in=alex&username__in=aaron"), [3]), + ({"username__in": ""}, [1, 2, 3, 4]), + ({"username__in": ","}, []), + ({"username__in": "alex"}, [1]), + ({"username__in": "alex,aaron"}, [1, 3]), + ({"username__in": "alex,,aaron"}, [1, 3]), + ({"username__in": "alex,"}, [1]), ] for params, expected in cases: with self.subTest(params=params, expected=expected): - self.assertQuerysetEqual(F(params, queryset=qs).qs, - expected, attrgetter('pk')) + self.assertQuerySetEqual( + F(params, queryset=qs).qs, expected, attrgetter("pk") + ) def test_datetime_filtering(self): F = self.article_filter - qs = Article.objects.order_by('pk') + qs = Article.objects.order_by("pk") after = self.after_5pm before = self.before_5pm cases = [ (None, [1, 2, 3, 4]), - (QueryDict('published__in=%s&published__in=%s' % (after, before)), [3, 4]), - ({'published__in': ''}, [1, 2, 3, 4]), - ({'published__in': ','}, []), - ({'published__in': '%s' % (after, )}, [1, 2]), - ({'published__in': '%s,%s' % (after, before, )}, [1, 2, 3, 4]), - ({'published__in': '%s,,%s' % (after, before, )}, [1, 2, 3, 4]), - ({'published__in': '%s,' % (after, )}, [1, 2]), + (QueryDict("published__in=%s&published__in=%s" % (after, before)), [3, 4]), + ({"published__in": ""}, [1, 2, 3, 4]), + ({"published__in": ","}, []), + ({"published__in": "%s" % (after,)}, [1, 2]), + ( + { + "published__in": "%s,%s" + % ( + after, + before, + ) + }, + [1, 2, 3, 4], + ), + ( + { + "published__in": "%s,,%s" + % ( + after, + before, + ) + }, + [1, 2, 3, 4], + ), + ({"published__in": "%s," % (after,)}, [1, 2]), ] for params, expected in cases: with self.subTest(params=params, expected=expected): - self.assertQuerysetEqual(F(params, queryset=qs).qs, - expected, attrgetter('pk')) + self.assertQuerySetEqual( + F(params, queryset=qs).qs, expected, attrgetter("pk") + ) def test_related_filtering(self): F = self.article_filter - qs = Article.objects.order_by('pk') + qs = Article.objects.order_by("pk") cases = [ (None, [1, 2, 3, 4]), - (QueryDict('author__in=1&author__in=2'), [2, 4]), - ({'author__in': ''}, [1, 2, 3, 4]), - ({'author__in': ','}, []), - ({'author__in': '1'}, [1, 3]), - ({'author__in': '1,2'}, [1, 2, 3, 4]), - ({'author__in': '1,,2'}, [1, 2, 3, 4]), - ({'author__in': '1,'}, [1, 3]), + (QueryDict("author__in=1&author__in=2"), [2, 4]), + ({"author__in": ""}, [1, 2, 3, 4]), + ({"author__in": ","}, []), + ({"author__in": "1"}, [1, 3]), + ({"author__in": "1,2"}, [1, 2, 3, 4]), + ({"author__in": "1,,2"}, [1, 2, 3, 4]), + ({"author__in": "1,"}, [1, 3]), ] for params, expected in cases: with self.subTest(params=params, expected=expected): - self.assertQuerysetEqual(F(params, queryset=qs).qs, - expected, attrgetter('pk')) + self.assertQuerySetEqual( + F(params, queryset=qs).qs, expected, attrgetter("pk") + ) -@override_settings(TIME_ZONE='UTC') +@override_settings(TIME_ZONE="UTC") class CSVRangeFilterTests(TestCase): - class ArticleFilter(FilterSet): class Meta: model = Article fields = { - 'published': ['range'], + "published": ["range"], } @classmethod def setUpTestData(cls): - u1 = User.objects.create(username='alex', status=1) - u2 = User.objects.create(username='jacob', status=2) - User.objects.create(username='aaron', status=2) - User.objects.create(username='carl', status=0) + u1 = User.objects.create(username="alex", status=1) + u2 = User.objects.create(username="jacob", status=2) + User.objects.create(username="aaron", status=2) + User.objects.create(username="carl", status=0) now_dt = now() after_5pm = now_dt.replace(hour=18) @@ -1835,9 +1911,9 @@ def setUpTestData(cls): Article.objects.create(author=u1, published=around_5pm) Article.objects.create(author=u2, published=before_5pm) - cls.after_5pm = after_5pm.strftime('%Y-%m-%d %H:%M:%S.%f') - cls.around_5pm = around_5pm.strftime('%Y-%m-%d %H:%M:%S.%f') - cls.before_5pm = before_5pm.strftime('%Y-%m-%d %H:%M:%S.%f') + cls.after_5pm = after_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") + cls.around_5pm = around_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") + cls.before_5pm = before_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") def test_filtering(self): F = self.ArticleFilter @@ -1846,108 +1922,144 @@ def test_filtering(self): self.assertEqual(f.qs.count(), 4) # empty value is a noop - f = F({'published__range': ''}) + f = F({"published__range": ""}) self.assertTrue(f.is_valid()) self.assertEqual(f.qs.count(), 4) # empty values are interpreted as None types - f = F({'published__range': ','}) + f = F({"published__range": ","}) self.assertEqual(f.qs.count(), 0) - f = F({'published__range': '%s' % (self.before_5pm, )}) + f = F({"published__range": "%s" % (self.before_5pm,)}) self.assertFalse(f.is_valid()) - f = F({'published__range': '%s,%s' % (self.before_5pm, self.around_5pm, )}) + f = F( + { + "published__range": "%s,%s" + % ( + self.before_5pm, + self.around_5pm, + ) + } + ) self.assertEqual(f.qs.count(), 3) - f = F({'published__range': '%s,,%s' % (self.before_5pm, self.after_5pm, )}) + f = F( + { + "published__range": "%s,,%s" + % ( + self.before_5pm, + self.after_5pm, + ) + } + ) self.assertFalse(f.is_valid()) # empty value is interpreted as None type - f = F({'published__range': '%s,' % (self.before_5pm, )}) + f = F({"published__range": "%s," % (self.before_5pm,)}) self.assertEqual(f.qs.count(), 0) class OrderingFilterTests(TestCase): - def setUp(self): - User.objects.create(username='alex', status=1) - User.objects.create(username='jacob', status=2) - User.objects.create(username='aaron', status=2) - User.objects.create(username='carl', status=0) + User.objects.create(username="alex", status=1) + User.objects.create(username="jacob", status=2) + User.objects.create(username="aaron", status=2) + User.objects.create(username="carl", status=0) def test_ordering(self): class F(FilterSet): - o = OrderingFilter( - fields=('username', ) - ) + o = OrderingFilter(fields=("username",)) class Meta: model = User - fields = ['username'] + fields = ["username"] qs = User.objects.all() - f = F({'o': 'username'}, queryset=qs) - names = f.qs.values_list('username', flat=True) - self.assertEqual(list(names), ['aaron', 'alex', 'carl', 'jacob']) + tests = [ + {"o": "username"}, + QueryDict("o=username,"), + ] + for data in tests: + with self.subTest(data=data): + f = F(data, queryset=qs) + names = f.qs.values_list("username", flat=True) + self.assertEqual(list(names), ["aaron", "alex", "carl", "jacob"]) def test_ordering_with_select_widget(self): class F(FilterSet): - o = OrderingFilter( - widget=forms.Select, - fields=('username', ) - ) + o = OrderingFilter(widget=forms.Select, fields=("username",)) class Meta: model = User - fields = ['username'] + fields = ["username"] qs = User.objects.all() - f = F({'o': 'username'}, queryset=qs) - names = f.qs.values_list('username', flat=True) - self.assertEqual(list(names), ['aaron', 'alex', 'carl', 'jacob']) + f = F({"o": "username"}, queryset=qs) + names = f.qs.values_list("username", flat=True) + self.assertEqual(list(names), ["aaron", "alex", "carl", "jacob"]) + def test_csv_input(self): + class F(FilterSet): + o = OrderingFilter(widget=forms.Select, fields=("username",),) -class MiscFilterSetTests(TestCase): + class Meta: + model = User + fields = ["username"] + qs = User.objects.all() + tests = [ + {"o": ","}, + QueryDict("o=%2c"), + QueryDict("o=,"), + ] + for data in tests: + with self.subTest(data=data): + f = F(data, queryset=qs) + self.assertIs(True, f.is_valid()) + names = f.qs.values_list("username", flat=True) + self.assertEqual(list(names), ['alex', 'jacob', 'aaron', 'carl']) + + +class MiscFilterSetTests(TestCase): def setUp(self): - User.objects.create(username='alex', status=1) - User.objects.create(username='jacob', status=2) - User.objects.create(username='aaron', status=2) - User.objects.create(username='carl', status=0) + User.objects.create(username="alex", last_name="johnson", status=1) + User.objects.create(username="jacob", last_name="johnson", status=2) + User.objects.create(username="aaron", last_name="white", status=2) + User.objects.create(username="carl", last_name="black", status=0) def test_filtering_with_declared_filters(self): class F(FilterSet): - account = CharFilter(field_name='username') + account = CharFilter(field_name="username") class Meta: model = User - fields = ['account'] + fields = ["account"] qs = MockQuerySet() - F({'account': 'jdoe'}, queryset=qs).qs - qs.all.return_value.filter.assert_called_with(username__exact='jdoe') + F({"account": "jdoe"}, queryset=qs).qs + qs.all.return_value.filter.assert_called_with(username__exact="jdoe") def test_filtering_without_meta(self): class F(FilterSet): username = CharFilter() - f = F({'username': 'alex'}, queryset=User.objects.all()) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"username": "alex"}, queryset=User.objects.all()) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) def test_filtering_with_multiple_filters(self): class F(FilterSet): class Meta: model = User - fields = ['status', 'username'] + fields = ["status", "username"] qs = User.objects.all() - f = F({'username': 'alex', 'status': '1'}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username) + f = F({"username": "alex", "status": "1"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) - f = F({'username': 'alex', 'status': '2'}, queryset=qs) - self.assertQuerysetEqual(f.qs, [], lambda o: o.pk) + f = F({"username": "alex", "status": "2"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [], lambda o: o.pk) def test_filter_with_initial(self): # Initial values are a form presentation option - the FilterSet should @@ -1957,36 +2069,69 @@ class F(FilterSet): class Meta: model = User - fields = ['status'] + fields = ["status"] qs = User.objects.all() - users = ['alex', 'jacob', 'aaron', 'carl'] + users = ["alex", "jacob", "aaron", "carl"] f = F(queryset=qs) - self.assertQuerysetEqual(f.qs.order_by('pk'), users, lambda o: o.username) + self.assertQuerySetEqual(f.qs.order_by("pk"), users, lambda o: o.username) - f = F({'status': 0}, queryset=qs) - self.assertQuerysetEqual(f.qs, ['carl'], lambda o: o.username) + f = F({"status": 0}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username) def test_qs_count(self): class F(FilterSet): class Meta: model = User - fields = ['status'] + fields = ["status"] qs = User.objects.all() f = F(queryset=qs) self.assertEqual(len(f.qs), 4) self.assertEqual(f.qs.count(), 4) - f = F({'status': '0'}, queryset=qs) + f = F({"status": "0"}, queryset=qs) self.assertEqual(len(f.qs), 1) self.assertEqual(f.qs.count(), 1) - f = F({'status': '1'}, queryset=qs) + f = F({"status": "1"}, queryset=qs) self.assertEqual(len(f.qs), 1) self.assertEqual(f.qs.count(), 1) - f = F({'status': '2'}, queryset=qs) + f = F({"status": "2"}, queryset=qs) self.assertEqual(len(f.qs), 2) self.assertEqual(f.qs.count(), 2) + + def test_filtering_with_widgets(self): + class CharInFilter(BaseInFilter, CharFilter): + pass + + class F(FilterSet): + last_name = CharInFilter(widget=QueryArrayWidget) + username = CharInFilter() + + class Meta: + model = User + fields = ["last_name", "username"] + + qs = User.objects.all() + + f = F({"last_name": ["johnson"]}, queryset=qs) + self.assertQuerySetEqual( + f.qs, ["alex", "jacob"], lambda o: o.username, ordered=False + ) + + f = F({"last_name": ["johnson"], "username": "carl"}, queryset=qs) + self.assertQuerySetEqual(f.qs, [], lambda o: o.username, ordered=False) + + f = F({"last_name": ["johnson"], "username": "jacob"}, queryset=qs) + self.assertQuerySetEqual(f.qs, ["jacob"], lambda o: o.username, ordered=False) + + f = F( + {"last_name": ["johnson", "white"], "username": "jacob, carl, aaron"}, + queryset=qs, + ) + self.assertQuerySetEqual( + f.qs, ["jacob", "aaron"], lambda o: o.username, ordered=False + ) diff --git a/tests/test_filters.py b/tests/test_filters.py index d63074d7f..290fbc135 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,7 +1,7 @@ import inspect -import mock from collections import OrderedDict from datetime import date, datetime, time, timedelta +from unittest import mock from django import forms from django.test import TestCase, override_settings @@ -16,7 +16,7 @@ IsoDateTimeRangeField, Lookup, RangeField, - TimeRangeField + TimeRangeField, ) from django_filters.filters import ( AllValuesFilter, @@ -45,38 +45,35 @@ TimeFilter, TimeRangeFilter, TypedMultipleChoiceFilter, - UUIDFilter + UUIDFilter, ) from tests.models import Book, User class ModuleImportTests(TestCase): def is_filter(self, name, value): - return ( - isinstance(value, type) and issubclass(value, Filter) - ) + return isinstance(value, type) and issubclass(value, Filter) def test_imports(self): # msg = "Expected `filters.%s` to be imported in `filters.__all__`" filter_classes = [ - key for key, value - in inspect.getmembers(filters) + key + for key, value in inspect.getmembers(filters) if isinstance(value, type) and issubclass(value, Filter) ] # sanity check - self.assertIn('Filter', filter_classes) - self.assertIn('BooleanFilter', filter_classes) + self.assertIn("Filter", filter_classes) + self.assertIn("BooleanFilter", filter_classes) for f in filter_classes: self.assertIn(f, filters.__all__) class FilterTests(TestCase): - def test_creation(self): f = Filter() - self.assertEqual(f.lookup_expr, 'exact') + self.assertEqual(f.lookup_expr, "exact") self.assertEqual(f.exclude, False) def test_creation_order(self): @@ -90,63 +87,58 @@ def test_default_field(self): self.assertIsInstance(field, forms.Field) def test_field_with_single_lookup_expr(self): - f = Filter(lookup_expr='iexact') + f = Filter(lookup_expr="iexact") field = f.field self.assertIsInstance(field, forms.Field) def test_field_params(self): - with mock.patch.object(Filter, 'field_class', - spec=['__call__']) as mocked: - f = Filter(field_name='somefield', label='somelabel', - widget='somewidget') + with mock.patch.object(Filter, "field_class", spec=["__call__"]) as mocked: + f = Filter(field_name="somefield", label="somelabel", widget="somewidget") f.field - mocked.assert_called_once_with(required=False, - label='somelabel', - widget='somewidget') + mocked.assert_called_once_with( + required=False, label="somelabel", widget="somewidget" + ) def test_field_extra_params(self): - with mock.patch.object(Filter, 'field_class', - spec=['__call__']) as mocked: - f = Filter(someattr='someattr') + with mock.patch.object(Filter, "field_class", spec=["__call__"]) as mocked: + f = Filter(someattr="someattr") f.field - mocked.assert_called_once_with(required=mock.ANY, - label=mock.ANY, - someattr='someattr') + mocked.assert_called_once_with( + required=mock.ANY, label=mock.ANY, someattr="someattr" + ) def test_field_required_default(self): # filter form fields should not be required by default - with mock.patch.object(Filter, 'field_class', - spec=['__call__']) as mocked: + with mock.patch.object(Filter, "field_class", spec=["__call__"]) as mocked: f = Filter() f.field - mocked.assert_called_once_with(required=False, - label=mock.ANY) + mocked.assert_called_once_with(required=False, label=mock.ANY) def test_filtering(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = Filter() - result = f.filter(qs, 'value') - qs.filter.assert_called_once_with(None__exact='value') + result = f.filter(qs, "value") + qs.filter.assert_called_once_with(None__exact="value") self.assertNotEqual(qs, result) def test_filtering_exclude(self): - qs = mock.Mock(spec=['filter', 'exclude']) + qs = mock.Mock(spec=["filter", "exclude"]) f = Filter(exclude=True) - result = f.filter(qs, 'value') - qs.exclude.assert_called_once_with(None__exact='value') + result = f.filter(qs, "value") + qs.exclude.assert_called_once_with(None__exact="value") self.assertNotEqual(qs, result) def test_filtering_uses_name(self): - qs = mock.Mock(spec=['filter']) - f = Filter(field_name='somefield') - f.filter(qs, 'value') - result = qs.filter.assert_called_once_with(somefield__exact='value') + qs = mock.Mock(spec=["filter"]) + f = Filter(field_name="somefield") + f.filter(qs, "value") + result = qs.filter.assert_called_once_with(somefield__exact="value") self.assertNotEqual(qs, result) def test_filtering_skipped_with_blank_value(self): qs = mock.Mock() f = Filter() - result = f.filter(qs, '') + result = f.filter(qs, "") self.assertListEqual(qs.method_calls, []) self.assertEqual(qs, result) @@ -161,20 +153,19 @@ def test_filter_using_method(self): qs = mock.NonCallableMock(spec=[]) method = mock.Mock() f = Filter(method=method) - result = f.filter(qs, 'value') - method.assert_called_once_with(qs, None, 'value') + result = f.filter(qs, "value") + method.assert_called_once_with(qs, None, "value") self.assertNotEqual(qs, result) def test_filtering_uses_distinct(self): - qs = mock.Mock(spec=['filter', 'distinct']) - f = Filter(field_name='somefield', distinct=True) - f.filter(qs, 'value') + qs = mock.Mock(spec=["filter", "distinct"]) + f = Filter(field_name="somefield", distinct=True) + f.filter(qs, "value") result = qs.distinct.assert_called_once_with() self.assertNotEqual(qs, result) class CharFilterTests(TestCase): - def test_default_field(self): f = CharFilter() field = f.field @@ -182,7 +173,6 @@ def test_default_field(self): class UUIDFilterTests(TestCase): - def test_default_field(self): f = UUIDFilter() field = f.field @@ -190,50 +180,48 @@ def test_default_field(self): class BooleanFilterTests(TestCase): - def test_default_field(self): f = BooleanFilter() field = f.field self.assertIsInstance(field, forms.NullBooleanField) def test_filtering(self): - qs = mock.Mock(spec=['filter']) - f = BooleanFilter(field_name='somefield') + qs = mock.Mock(spec=["filter"]) + f = BooleanFilter(field_name="somefield") result = f.filter(qs, True) qs.filter.assert_called_once_with(somefield__exact=True) self.assertNotEqual(qs, result) def test_filtering_exclude(self): - qs = mock.Mock(spec=['exclude']) - f = BooleanFilter(field_name='somefield', exclude=True) + qs = mock.Mock(spec=["exclude"]) + f = BooleanFilter(field_name="somefield", exclude=True) result = f.filter(qs, True) qs.exclude.assert_called_once_with(somefield__exact=True) self.assertNotEqual(qs, result) def test_filtering_skipped_with_blank_value(self): qs = mock.Mock() - f = BooleanFilter(field_name='somefield') - result = f.filter(qs, '') + f = BooleanFilter(field_name="somefield") + result = f.filter(qs, "") self.assertListEqual(qs.method_calls, []) self.assertEqual(qs, result) def test_filtering_skipped_with_none_value(self): qs = mock.Mock() - f = BooleanFilter(field_name='somefield') + f = BooleanFilter(field_name="somefield") result = f.filter(qs, None) self.assertListEqual(qs.method_calls, []) self.assertEqual(qs, result) def test_filtering_lookup_expr(self): - qs = mock.Mock(spec=['filter']) - f = BooleanFilter(field_name='somefield', lookup_expr='isnull') + qs = mock.Mock(spec=["filter"]) + f = BooleanFilter(field_name="somefield", lookup_expr="isnull") result = f.filter(qs, True) qs.filter.assert_called_once_with(somefield__isnull=True) self.assertNotEqual(qs, result) class ChoiceFilterTests(TestCase): - def test_default_field(self): f = ChoiceFilter() field = f.field @@ -241,131 +229,182 @@ def test_default_field(self): def test_empty_choice(self): # default value - f = ChoiceFilter(choices=[('a', 'a')]) - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('a', 'a'), - ]) + f = ChoiceFilter(choices=[("a", "a")]) + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("a", "a"), + ], + ) # set value, allow blank label - f = ChoiceFilter(choices=[('a', 'a')], empty_label='') - self.assertEqual(list(f.field.choices), [ - ('', ''), - ('a', 'a'), - ]) + f = ChoiceFilter(choices=[("a", "a")], empty_label="") + self.assertEqual( + list(f.field.choices), + [ + ("", ""), + ("a", "a"), + ], + ) # disable empty choice w/ None - f = ChoiceFilter(choices=[('a', 'a')], empty_label=None) - self.assertEqual(list(f.field.choices), [ - ('a', 'a'), - ]) + f = ChoiceFilter(choices=[("a", "a")], empty_label=None) + self.assertEqual( + list(f.field.choices), + [ + ("a", "a"), + ], + ) def test_null_choice(self): # default is to be disabled - f = ChoiceFilter(choices=[('a', 'a')], ) - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('a', 'a'), - ]) + f = ChoiceFilter( + choices=[("a", "a")], + ) + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("a", "a"), + ], + ) # set label, allow blank label - f = ChoiceFilter(choices=[('a', 'a')], null_label='') - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('null', ''), - ('a', 'a'), - ]) + f = ChoiceFilter(choices=[("a", "a")], null_label="") + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("null", ""), + ("a", "a"), + ], + ) # set null value - f = ChoiceFilter(choices=[('a', 'a')], null_value='NULL', null_label='') - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('NULL', ''), - ('a', 'a'), - ]) + f = ChoiceFilter(choices=[("a", "a")], null_value="NULL", null_label="") + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("NULL", ""), + ("a", "a"), + ], + ) # explicitly disable - f = ChoiceFilter(choices=[('a', 'a')], null_label=None) - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('a', 'a'), - ]) + f = ChoiceFilter(choices=[("a", "a")], null_label=None) + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("a", "a"), + ], + ) def test_null_multiplechoice(self): # default is to be disabled - f = MultipleChoiceFilter(choices=[('a', 'a')], ) - self.assertEqual(list(f.field.choices), [ - ('a', 'a'), - ]) + f = MultipleChoiceFilter( + choices=[("a", "a")], + ) + self.assertEqual( + list(f.field.choices), + [ + ("a", "a"), + ], + ) # set label, allow blank label - f = MultipleChoiceFilter(choices=[('a', 'a')], null_label='') - self.assertEqual(list(f.field.choices), [ - ('null', ''), - ('a', 'a'), - ]) + f = MultipleChoiceFilter(choices=[("a", "a")], null_label="") + self.assertEqual( + list(f.field.choices), + [ + ("null", ""), + ("a", "a"), + ], + ) # set null value - f = MultipleChoiceFilter(choices=[('a', 'a')], null_value='NULL', null_label='') - self.assertEqual(list(f.field.choices), [ - ('NULL', ''), - ('a', 'a'), - ]) + f = MultipleChoiceFilter(choices=[("a", "a")], null_value="NULL", null_label="") + self.assertEqual( + list(f.field.choices), + [ + ("NULL", ""), + ("a", "a"), + ], + ) # explicitly disable - f = MultipleChoiceFilter(choices=[('a', 'a')], null_label=None) - self.assertEqual(list(f.field.choices), [ - ('a', 'a'), - ]) + f = MultipleChoiceFilter(choices=[("a", "a")], null_label=None) + self.assertEqual( + list(f.field.choices), + [ + ("a", "a"), + ], + ) @override_settings( - FILTERS_EMPTY_CHOICE_LABEL='EMPTY LABEL', - FILTERS_NULL_CHOICE_LABEL='NULL LABEL', - FILTERS_NULL_CHOICE_VALUE='NULL VALUE', ) + FILTERS_EMPTY_CHOICE_LABEL="EMPTY LABEL", + FILTERS_NULL_CHOICE_LABEL="NULL LABEL", + FILTERS_NULL_CHOICE_VALUE="NULL VALUE", + ) def test_settings_overrides(self): - f = ChoiceFilter(choices=[('a', 'a')], ) - self.assertEqual(list(f.field.choices), [ - ('', 'EMPTY LABEL'), - ('NULL VALUE', 'NULL LABEL'), - ('a', 'a'), - ]) - - f = MultipleChoiceFilter(choices=[('a', 'a')], ) - self.assertEqual(list(f.field.choices), [ - ('NULL VALUE', 'NULL LABEL'), - ('a', 'a'), - ]) + f = ChoiceFilter( + choices=[("a", "a")], + ) + self.assertEqual( + list(f.field.choices), + [ + ("", "EMPTY LABEL"), + ("NULL VALUE", "NULL LABEL"), + ("a", "a"), + ], + ) + + f = MultipleChoiceFilter( + choices=[("a", "a")], + ) + self.assertEqual( + list(f.field.choices), + [ + ("NULL VALUE", "NULL LABEL"), + ("a", "a"), + ], + ) def test_callable_choices(self): def choices(): - yield ('a', 'a') - yield ('b', 'b') + yield ("a", "a") + yield ("b", "b") f = ChoiceFilter(choices=choices) - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('a', 'a'), - ('b', 'b'), - ]) + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("a", "a"), + ("b", "b"), + ], + ) def test_callable_choices_is_lazy(self): def choices(): - self.fail('choices should not be called during initialization') + self.fail("choices should not be called during initialization") + ChoiceFilter(choices=choices) class MultipleChoiceFilterTests(TestCase): - def test_default_field(self): f = MultipleChoiceFilter() field = f.field self.assertIsInstance(field, forms.MultipleChoiceField) def test_filtering_requires_name(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = MultipleChoiceFilter() with self.assertRaises(TypeError): - f.filter(qs, ['value']) + f.filter(qs, ["value"]) def test_conjoined_default_value(self): f = MultipleChoiceFilter() @@ -378,72 +417,78 @@ def test_conjoined_true(self): def test_is_noop_false(self): f = MultipleChoiceFilter(required=False) f.always_filter = False - self.assertFalse(f.is_noop(None, ['value'])) + self.assertFalse(f.is_noop(None, ["value"])) def test_filtering(self): - qs = mock.Mock(spec=['filter']) - f = MultipleChoiceFilter(field_name='somefield') - with mock.patch('django_filters.filters.Q') as mockQclass: + qs = mock.Mock(spec=["filter"]) + f = MultipleChoiceFilter(field_name="somefield") + with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] - f.filter(qs, ['value']) + f.filter(qs, ["value"]) - self.assertEqual(mockQclass.call_args_list, - [mock.call(), mock.call(somefield='value')]) + self.assertEqual( + mockQclass.call_args_list, [mock.call(), mock.call(somefield="value")] + ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.filter.assert_called_once_with(mockQ1.__ior__.return_value) qs.filter.return_value.distinct.assert_called_once_with() def test_filtering_exclude(self): - qs = mock.Mock(spec=['exclude']) - f = MultipleChoiceFilter(field_name='somefield', exclude=True) - with mock.patch('django_filters.filters.Q') as mockQclass: + qs = mock.Mock(spec=["exclude"]) + f = MultipleChoiceFilter(field_name="somefield", exclude=True) + with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] - f.filter(qs, ['value']) + f.filter(qs, ["value"]) - self.assertEqual(mockQclass.call_args_list, - [mock.call(), mock.call(somefield='value')]) + self.assertEqual( + mockQclass.call_args_list, [mock.call(), mock.call(somefield="value")] + ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.exclude.assert_called_once_with(mockQ1.__ior__.return_value) qs.exclude.return_value.distinct.assert_called_once_with() def test_filtering_with_lookup_expr(self): - qs = mock.Mock(spec=['filter']) - f = MultipleChoiceFilter(field_name='somefield', lookup_expr='icontains') - with mock.patch('django_filters.filters.Q') as mockQclass: + qs = mock.Mock(spec=["filter"]) + f = MultipleChoiceFilter(field_name="somefield", lookup_expr="icontains") + with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] - f.filter(qs, ['value']) + f.filter(qs, ["value"]) - self.assertEqual(mockQclass.call_args_list, - [mock.call(), mock.call(somefield__icontains='value')]) + self.assertEqual( + mockQclass.call_args_list, + [mock.call(), mock.call(somefield__icontains="value")], + ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.filter.assert_called_once_with(mockQ1.__ior__.return_value) qs.filter.return_value.distinct.assert_called_once_with() - def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices(self): + def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices( + self, + ): qs = mock.Mock(spec=[]) - f = MultipleChoiceFilter(field_name='somefield', required=True) + f = MultipleChoiceFilter(field_name="somefield", required=True) f.always_filter = False result = f.filter(qs, []) self.assertEqual(len(f.field.choices), 0) self.assertEqual(qs, result) - f.field.choices = ['some', 'values', 'here'] - result = f.filter(qs, ['some', 'values', 'here']) + f.field.choices = ["some", "values", "here"] + result = f.filter(qs, ["some", "values", "here"]) self.assertEqual(qs, result) - result = f.filter(qs, ['other', 'values', 'there']) + result = f.filter(qs, ["other", "values", "there"]) self.assertEqual(qs, result) def test_filtering_skipped_with_empty_list_value_and_some_choices(self): qs = mock.Mock(spec=[]) - f = MultipleChoiceFilter(field_name='somefield') - f.field.choices = ['some', 'values', 'here'] + f = MultipleChoiceFilter(field_name="somefield") + f.field.choices = ["some", "values", "here"] result = f.filter(qs, []) self.assertEqual(qs, result) @@ -453,7 +498,7 @@ def test_filter_conjoined_true(self): users that have all of this books. """ - book_kwargs = {'price': 1, 'average_rating': 1} + book_kwargs = {"price": 1, "average_rating": 1} books = [] books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) @@ -475,54 +520,72 @@ def test_filter_conjoined_true(self): user5.favorite_books.add(books[4], books[5]) filter_list = ( - ((books[0].pk, books[0].pk), # values - [1, 2]), # list of user.pk that have `value` books - ((books[1].pk, books[1].pk), - [1, 2, 3]), - ((books[2].pk, books[2].pk), - [2, 3, 4]), - ((books[3].pk, books[3].pk), - [4, ]), - ((books[4].pk, books[4].pk), - [5, ]), - ((books[0].pk, books[1].pk), - [1, 2]), - ((books[0].pk, books[2].pk), - [2, ]), - ((books[1].pk, books[2].pk), - [2, 3]), - ((books[2].pk, books[3].pk), - [4, ]), - ((books[4].pk, books[5].pk), - [5, ]), - ((books[3].pk, books[4].pk), - []), + ( + (books[0].pk, books[0].pk), # values + [1, 2], + ), # list of user.pk that have `value` books + ((books[1].pk, books[1].pk), [1, 2, 3]), + ((books[2].pk, books[2].pk), [2, 3, 4]), + ( + (books[3].pk, books[3].pk), + [ + 4, + ], + ), + ( + (books[4].pk, books[4].pk), + [ + 5, + ], + ), + ((books[0].pk, books[1].pk), [1, 2]), + ( + (books[0].pk, books[2].pk), + [ + 2, + ], + ), + ((books[1].pk, books[2].pk), [2, 3]), + ( + (books[2].pk, books[3].pk), + [ + 4, + ], + ), + ( + (books[4].pk, books[5].pk), + [ + 5, + ], + ), + ((books[3].pk, books[4].pk), []), ) users = User.objects.all() for item in filter_list: - f = MultipleChoiceFilter(field_name='favorite_books__pk', conjoined=True) + f = MultipleChoiceFilter(field_name="favorite_books__pk", conjoined=True) queryset = f.filter(users, item[0]) - expected_pks = [c[0] for c in queryset.values_list('pk')] + expected_pks = [c[0] for c in queryset.values_list("pk")] self.assertListEqual( expected_pks, item[1], - 'Lists Differ: {0} != {1} for case {2}'.format( - expected_pks, item[1], item[0])) + "Lists Differ: {0} != {1} for case {2}".format( + expected_pks, item[1], item[0] + ), + ) class TypedMultipleChoiceFilterTests(TestCase): - def test_default_field(self): f = TypedMultipleChoiceFilter() field = f.field self.assertIsInstance(field, forms.TypedMultipleChoiceField) def test_filtering_requires_name(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = TypedMultipleChoiceFilter() with self.assertRaises(TypeError): - f.filter(qs, ['value']) + f.filter(qs, ["value"]) def test_conjoined_default_value(self): f = TypedMultipleChoiceFilter() @@ -533,54 +596,58 @@ def test_conjoined_true(self): self.assertTrue(f.conjoined) def test_filtering(self): - qs = mock.Mock(spec=['filter']) - f = TypedMultipleChoiceFilter(field_name='somefield') - with mock.patch('django_filters.filters.Q') as mockQclass: + qs = mock.Mock(spec=["filter"]) + f = TypedMultipleChoiceFilter(field_name="somefield") + with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] - f.filter(qs, ['value']) + f.filter(qs, ["value"]) - self.assertEqual(mockQclass.call_args_list, - [mock.call(), mock.call(somefield='value')]) + self.assertEqual( + mockQclass.call_args_list, [mock.call(), mock.call(somefield="value")] + ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.filter.assert_called_once_with(mockQ1.__ior__.return_value) qs.filter.return_value.distinct.assert_called_once_with() def test_filtering_exclude(self): - qs = mock.Mock(spec=['exclude']) - f = TypedMultipleChoiceFilter(field_name='somefield', exclude=True) - with mock.patch('django_filters.filters.Q') as mockQclass: + qs = mock.Mock(spec=["exclude"]) + f = TypedMultipleChoiceFilter(field_name="somefield", exclude=True) + with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] - f.filter(qs, ['value']) + f.filter(qs, ["value"]) - self.assertEqual(mockQclass.call_args_list, - [mock.call(), mock.call(somefield='value')]) + self.assertEqual( + mockQclass.call_args_list, [mock.call(), mock.call(somefield="value")] + ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.exclude.assert_called_once_with(mockQ1.__ior__.return_value) qs.exclude.return_value.distinct.assert_called_once_with() - def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices(self): + def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices( + self, + ): qs = mock.Mock(spec=[]) - f = TypedMultipleChoiceFilter(field_name='somefield', required=True) + f = TypedMultipleChoiceFilter(field_name="somefield", required=True) f.always_filter = False result = f.filter(qs, []) self.assertEqual(len(f.field.choices), 0) self.assertEqual(qs, result) - f.field.choices = ['some', 'values', 'here'] - result = f.filter(qs, ['some', 'values', 'here']) + f.field.choices = ["some", "values", "here"] + result = f.filter(qs, ["some", "values", "here"]) self.assertEqual(qs, result) - result = f.filter(qs, ['other', 'values', 'there']) + result = f.filter(qs, ["other", "values", "there"]) self.assertEqual(qs, result) def test_filtering_skipped_with_empty_list_value_and_some_choices(self): qs = mock.Mock(spec=[]) - f = TypedMultipleChoiceFilter(field_name='somefield') - f.field.choices = ['some', 'values', 'here'] + f = TypedMultipleChoiceFilter(field_name="somefield") + f.field.choices = ["some", "values", "here"] result = f.filter(qs, []) self.assertEqual(qs, result) @@ -590,7 +657,7 @@ def test_filter_conjoined_true(self): users that have all of this books. """ - book_kwargs = {'price': 1, 'average_rating': 1} + book_kwargs = {"price": 1, "average_rating": 1} books = [] books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) @@ -612,44 +679,64 @@ def test_filter_conjoined_true(self): user5.favorite_books.add(books[4], books[5]) filter_list = ( - ((books[0].pk, books[0].pk), # values - [1, 2]), # list of user.pk that have `value` books - ((books[1].pk, books[1].pk), - [1, 2, 3]), - ((books[2].pk, books[2].pk), - [2, 3, 4]), - ((books[3].pk, books[3].pk), - [4, ]), - ((books[4].pk, books[4].pk), - [5, ]), - ((books[0].pk, books[1].pk), - [1, 2]), - ((books[0].pk, books[2].pk), - [2, ]), - ((books[1].pk, books[2].pk), - [2, 3]), - ((books[2].pk, books[3].pk), - [4, ]), - ((books[4].pk, books[5].pk), - [5, ]), - ((books[3].pk, books[4].pk), - []), + ( + (books[0].pk, books[0].pk), # values + [1, 2], + ), # list of user.pk that have `value` books + ((books[1].pk, books[1].pk), [1, 2, 3]), + ((books[2].pk, books[2].pk), [2, 3, 4]), + ( + (books[3].pk, books[3].pk), + [ + 4, + ], + ), + ( + (books[4].pk, books[4].pk), + [ + 5, + ], + ), + ((books[0].pk, books[1].pk), [1, 2]), + ( + (books[0].pk, books[2].pk), + [ + 2, + ], + ), + ((books[1].pk, books[2].pk), [2, 3]), + ( + (books[2].pk, books[3].pk), + [ + 4, + ], + ), + ( + (books[4].pk, books[5].pk), + [ + 5, + ], + ), + ((books[3].pk, books[4].pk), []), ) users = User.objects.all() for item in filter_list: - f = TypedMultipleChoiceFilter(field_name='favorite_books__pk', conjoined=True) + f = TypedMultipleChoiceFilter( + field_name="favorite_books__pk", conjoined=True + ) queryset = f.filter(users, item[0]) - expected_pks = [c[0] for c in queryset.values_list('pk')] + expected_pks = [c[0] for c in queryset.values_list("pk")] self.assertListEqual( expected_pks, item[1], - 'Lists Differ: {0} != {1} for case {2}'.format( - expected_pks, item[1], item[0])) + "Lists Differ: {0} != {1} for case {2}".format( + expected_pks, item[1], item[0] + ), + ) class DateFilterTests(TestCase): - def test_default_field(self): f = DateFilter() field = f.field @@ -657,7 +744,6 @@ def test_default_field(self): class DateTimeFilterTests(TestCase): - def test_default_field(self): f = DateTimeFilter() field = f.field @@ -665,7 +751,6 @@ def test_default_field(self): class TimeFilterTests(TestCase): - def test_default_field(self): f = TimeFilter() field = f.field @@ -673,7 +758,6 @@ def test_default_field(self): class DurationFilterTests(TestCase): - def test_default_field(self): f = DurationFilter() field = f.field @@ -691,21 +775,26 @@ def get_mock_queryset(self): class ModelChoiceFilterTests(TestCase, MockQuerySetMixin): - def test_default_field_without_queryset(self): f = ModelChoiceFilter() with self.assertRaises(TypeError): f.field @override_settings( - FILTERS_EMPTY_CHOICE_LABEL='EMPTY', - FILTERS_NULL_CHOICE_VALUE='NULL', ) + FILTERS_EMPTY_CHOICE_LABEL="EMPTY", + FILTERS_NULL_CHOICE_VALUE="NULL", + ) def test_empty_choices(self): - f = ModelChoiceFilter(queryset=User.objects.all(), null_value='null', null_label='NULL') - self.assertEqual(list(f.field.choices), [ - ('', 'EMPTY'), - ('null', 'NULL'), - ]) + f = ModelChoiceFilter( + queryset=User.objects.all(), null_value="null", null_label="NULL" + ) + self.assertEqual( + list(f.field.choices), + [ + ("", "EMPTY"), + ("null", "NULL"), + ], + ) def test_default_field_with_queryset(self): qs = self.get_mock_queryset() @@ -732,7 +821,9 @@ def test_get_queryset_override(self): qs = self.get_mock_queryset() class F(ModelChoiceFilter): - get_queryset = mock.create_autospec(ModelChoiceFilter.get_queryset, return_value=qs) + get_queryset = mock.create_autospec( + ModelChoiceFilter.get_queryset, return_value=qs + ) f = F() f.parent = mock.Mock(request=request) @@ -743,20 +834,25 @@ class F(ModelChoiceFilter): class ModelMultipleChoiceFilterTests(TestCase, MockQuerySetMixin): - def test_default_field_without_queryset(self): f = ModelMultipleChoiceFilter() with self.assertRaises(TypeError): f.field @override_settings( - FILTERS_EMPTY_CHOICE_LABEL='EMPTY', - FILTERS_NULL_CHOICE_VALUE='NULL', ) + FILTERS_EMPTY_CHOICE_LABEL="EMPTY", + FILTERS_NULL_CHOICE_VALUE="NULL", + ) def test_empty_choices(self): - f = ModelMultipleChoiceFilter(queryset=User.objects.all(), null_value='null', null_label='NULL') - self.assertEqual(list(f.field.choices), [ - ('null', 'NULL'), - ]) + f = ModelMultipleChoiceFilter( + queryset=User.objects.all(), null_value="null", null_label="NULL" + ) + self.assertEqual( + list(f.field.choices), + [ + ("null", "NULL"), + ], + ) def test_default_field_with_queryset(self): qs = self.get_mock_queryset() @@ -767,17 +863,17 @@ def test_default_field_with_queryset(self): def test_filtering_to_field_name(self): qs = User.objects.all() - f = ModelMultipleChoiceFilter(field_name='first_name', - to_field_name='first_name', - queryset=qs) - user = User.objects.create(first_name='Firstname') + f = ModelMultipleChoiceFilter( + field_name="first_name", to_field_name="first_name", queryset=qs + ) + user = User.objects.create(first_name="Firstname") - self.assertEqual(f.get_filter_predicate(user), - {'first_name': 'Firstname'}) - self.assertEqual(f.get_filter_predicate('FilterValue'), - {'first_name': 'FilterValue'}) + self.assertEqual(f.get_filter_predicate(user), {"first_name": "Firstname"}) + self.assertEqual( + f.get_filter_predicate("FilterValue"), {"first_name": "FilterValue"} + ) - self.assertEqual(list(f.filter(qs, ['Firstname'])), [user]) + self.assertEqual(list(f.filter(qs, ["Firstname"])), [user]) self.assertEqual(list(f.filter(qs, [user])), [user]) def test_callable_queryset(self): @@ -795,14 +891,13 @@ def test_callable_queryset(self): class NumberFilterTests(TestCase): - def test_default_field(self): f = NumberFilter() field = f.field self.assertIsInstance(field, forms.DecimalField) def test_filtering(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = NumberFilter() f.filter(qs, 1) qs.filter.assert_called_once_with(None__exact=1) @@ -812,7 +907,7 @@ def test_filtering(self): qs.filter.assert_called_once_with(None__exact=0) def test_filtering_exclude(self): - qs = mock.Mock(spec=['exclude']) + qs = mock.Mock(spec=["exclude"]) f = NumberFilter(exclude=True) f.filter(qs, 1) qs.exclude.assert_called_once_with(None__exact=1) @@ -823,28 +918,27 @@ def test_filtering_exclude(self): class NumericRangeFilterTests(TestCase): - def test_default_field(self): f = NumericRangeFilter() field = f.field self.assertIsInstance(field, RangeField) def test_filtering(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=20, stop=30) f = NumericRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__exact=(20, 30)) def test_filtering_exclude(self): - qs = mock.Mock(spec=['exclude']) + qs = mock.Mock(spec=["exclude"]) value = mock.Mock(start=20, stop=30) f = NumericRangeFilter(exclude=True) f.filter(qs, value) qs.exclude.assert_called_once_with(None__exact=(20, 30)) def test_filtering_skipped_with_none_value(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = NumericRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) @@ -852,26 +946,26 @@ def test_filtering_skipped_with_none_value(self): def test_field_with_lookup_expr(self): qs = mock.Mock() value = mock.Mock(start=20, stop=30) - f = NumericRangeFilter(lookup_expr=('overlap')) + f = NumericRangeFilter(lookup_expr=("overlap")) f.filter(qs, value) qs.filter.assert_called_once_with(None__overlap=(20, 30)) def test_zero_to_zero(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=0, stop=0) f = NumericRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__exact=(0, 0)) def test_filtering_startswith(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=20, stop=None) f = NumericRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__startswith=20) def test_filtering_endswith(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=30) f = NumericRangeFilter() f.filter(qs, value) @@ -900,42 +994,41 @@ def test_filtering_distinct(self): class RangeFilterTests(TestCase): - def test_default_field(self): f = RangeFilter() field = f.field self.assertIsInstance(field, RangeField) def test_filtering_range(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=20, stop=30) f = RangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__range=(20, 30)) def test_filtering_exclude(self): - qs = mock.Mock(spec=['exclude']) + qs = mock.Mock(spec=["exclude"]) value = mock.Mock(start=20, stop=30) f = RangeFilter(exclude=True) f.filter(qs, value) qs.exclude.assert_called_once_with(None__range=(20, 30)) def test_filtering_start(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=20, stop=None) f = RangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=20) def test_filtering_stop(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=30) f = RangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=30) def test_filtering_skipped_with_none_value(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = RangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) @@ -943,7 +1036,7 @@ def test_filtering_skipped_with_none_value(self): def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock(start=20, stop=30) - f = RangeFilter(lookup_expr='gte') + f = RangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with(None__range=(20, 30)) @@ -970,17 +1063,16 @@ def test_filtering_distinct(self): class DateRangeFilterTests(TestCase): - def test_creating(self): f = DateRangeFilter(empty_label=None) self.assertEqual(len(f.choices), 5) - self.assertIs(f.choices, f.extra['choices']) + self.assertIs(f.choices, f.extra["choices"]) f = DateRangeFilter(empty_label=None, choices=[], filters=[]) self.assertEqual(f.choices, []) self.assertEqual(f.filters, []) self.assertEqual(len(f.choices), 0) - self.assertIs(f.choices, f.extra['choices']) + self.assertIs(f.choices, f.extra["choices"]) def test_default_field(self): f = DateRangeFilter() @@ -991,81 +1083,76 @@ def test_filtering(self): # skip filtering, as it's an empty value qs = mock.Mock(spec=[]) f = DateRangeFilter() - result = f.filter(qs, '') + result = f.filter(qs, "") self.assertEqual(qs, result) def test_filtering_skipped_with_out_of_range_value(self): - # Field validation should prevent this from occuring + # Field validation should prevent this from occurring qs = mock.Mock(spec=[]) f = DateRangeFilter() with self.assertRaises(AssertionError): - f.filter(qs, 'tomorrow') + f.filter(qs, "tomorrow") def test_choices_and_filters_mismatch(self): - msg = "Keys must be present in both 'choices' and 'filters'. Missing keys: 'a, b'" + msg = ( + "Keys must be present in both 'choices' and 'filters'. Missing keys: 'a, b'" + ) with self.assertRaisesMessage(AssertionError, msg): - DateRangeFilter(choices=[('a', 'a')], filters={'b': None}) + DateRangeFilter(choices=[("a", "a")], filters={"b": None}) - def test_options_removed(self): - msg = "The 'options' attribute has been replaced by 'choices' and 'filters'. " \ - "See: https://django-filter.readthedocs.io/en/master/guide/migration.html" - - class F(DateRangeFilter): - options = None - - with self.assertRaisesMessage(AssertionError, msg): - F() + def test_choices_with_optgroups_dont_mistmatch(self): + DateRangeFilter( + choices=[("group", ("a", "a")), ("b", "b")], filters={"a": None, "b": None} + ) def test_filtering_for_this_year(self): - qs = mock.Mock(spec=['filter']) - with mock.patch('django_filters.filters.now') as mock_now: + qs = mock.Mock(spec=["filter"]) + with mock.patch("django_filters.filters.now") as mock_now: now_dt = mock_now.return_value f = DateRangeFilter() - f.filter(qs, 'year') - qs.filter.assert_called_once_with( - None__year=now_dt.year) + f.filter(qs, "year") + qs.filter.assert_called_once_with(None__year=now_dt.year) def test_filtering_for_this_month(self): - qs = mock.Mock(spec=['filter']) - with mock.patch('django_filters.filters.now') as mock_now: + qs = mock.Mock(spec=["filter"]) + with mock.patch("django_filters.filters.now") as mock_now: now_dt = mock_now.return_value f = DateRangeFilter() - f.filter(qs, 'month') + f.filter(qs, "month") qs.filter.assert_called_once_with( - None__year=now_dt.year, None__month=now_dt.month) + None__year=now_dt.year, None__month=now_dt.month + ) def test_filtering_for_7_days(self): - qs = mock.Mock(spec=['filter']) - with mock.patch('django_filters.filters.now'), \ - mock.patch('django_filters.filters.timedelta') as mock_td, \ - mock.patch('django_filters.filters._truncate') as mock_truncate: + qs = mock.Mock(spec=["filter"]) + with mock.patch("django_filters.filters.now"), mock.patch( + "django_filters.filters.timedelta" + ) as mock_td, mock.patch("django_filters.filters._truncate") as mock_truncate: mock_d1, mock_d2 = mock.MagicMock(), mock.MagicMock() mock_truncate.side_effect = [mock_d1, mock_d2] f = DateRangeFilter() - f.filter(qs, 'week') + f.filter(qs, "week") self.assertEqual( - mock_td.call_args_list, - [mock.call(days=7), mock.call(days=1)] + mock_td.call_args_list, [mock.call(days=7), mock.call(days=1)] ) qs.filter.assert_called_once_with(None__lt=mock_d2, None__gte=mock_d1) def test_filtering_for_today(self): - qs = mock.Mock(spec=['filter']) - with mock.patch('django_filters.filters.now') as mock_now: + qs = mock.Mock(spec=["filter"]) + with mock.patch("django_filters.filters.now") as mock_now: now_dt = mock_now.return_value f = DateRangeFilter() - f.filter(qs, 'today') + f.filter(qs, "today") qs.filter.assert_called_once_with( - None__year=now_dt.year, - None__month=now_dt.month, - None__day=now_dt.day) + None__year=now_dt.year, None__month=now_dt.month, None__day=now_dt.day + ) def test_filtering_for_yesterday(self): - qs = mock.Mock(spec=['filter']) - with mock.patch('django_filters.filters.now') as mock_now: + qs = mock.Mock(spec=["filter"]) + with mock.patch("django_filters.filters.now") as mock_now: now_dt = mock_now.return_value f = DateRangeFilter() - f.filter(qs, 'yesterday') + f.filter(qs, "yesterday") qs.filter.assert_called_once_with( None__year=(now_dt - timedelta(days=1)).year, None__month=(now_dt - timedelta(days=1)).month, @@ -1074,36 +1161,36 @@ def test_filtering_for_yesterday(self): class DateFromToRangeFilterTests(TestCase): - def test_default_field(self): f = DateFromToRangeFilter() field = f.field self.assertIsInstance(field, DateRangeField) def test_filtering_range(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=date(2015, 4, 7), stop=date(2015, 9, 6)) f = DateFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with( - None__range=(date(2015, 4, 7), date(2015, 9, 6))) + None__range=(date(2015, 4, 7), date(2015, 9, 6)) + ) def test_filtering_start(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=date(2015, 4, 7), stop=None) f = DateFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=date(2015, 4, 7)) def test_filtering_stop(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=date(2015, 9, 6)) f = DateFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=date(2015, 9, 6)) def test_filtering_skipped_with_none_value(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = DateFromToRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) @@ -1111,44 +1198,46 @@ def test_filtering_skipped_with_none_value(self): def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock(start=date(2015, 4, 7), stop=date(2015, 9, 6)) - f = DateFromToRangeFilter(lookup_expr='gte') + f = DateFromToRangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with( - None__range=(date(2015, 4, 7), date(2015, 9, 6))) + None__range=(date(2015, 4, 7), date(2015, 9, 6)) + ) class DateTimeFromToRangeFilterTests(TestCase): - def test_default_field(self): f = DateTimeFromToRangeFilter() field = f.field self.assertIsInstance(field, DateTimeRangeField) def test_filtering_range(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock( - start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45)) + start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45) + ) f = DateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with( - None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45))) + None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)) + ) def test_filtering_start(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=datetime(2015, 4, 7, 8, 30), stop=None) f = DateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=datetime(2015, 4, 7, 8, 30)) def test_filtering_stop(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=datetime(2015, 9, 6, 11, 45)) f = DateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=datetime(2015, 9, 6, 11, 45)) def test_filtering_skipped_with_none_value(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = DateTimeFromToRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) @@ -1156,45 +1245,48 @@ def test_filtering_skipped_with_none_value(self): def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock( - start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45)) - f = DateTimeFromToRangeFilter(lookup_expr='gte') + start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45) + ) + f = DateTimeFromToRangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with( - None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45))) + None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)) + ) class IsoDateTimeFromToRangeFilterTests(TestCase): - def test_default_field(self): f = IsoDateTimeFromToRangeFilter() field = f.field self.assertIsInstance(field, IsoDateTimeRangeField) def test_filtering_range(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock( - start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45)) + start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45) + ) f = IsoDateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with( - None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45))) + None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)) + ) def test_filtering_start(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=datetime(2015, 4, 7, 8, 30), stop=None) f = IsoDateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=datetime(2015, 4, 7, 8, 30)) def test_filtering_stop(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=datetime(2015, 9, 6, 11, 45)) f = IsoDateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=datetime(2015, 9, 6, 11, 45)) def test_filtering_skipped_with_none_value(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = IsoDateTimeFromToRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) @@ -1202,44 +1294,44 @@ def test_filtering_skipped_with_none_value(self): def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock( - start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45)) - f = IsoDateTimeFromToRangeFilter(lookup_expr='gte') + start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45) + ) + f = IsoDateTimeFromToRangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with( - None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45))) + None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)) + ) class TimeRangeFilterTests(TestCase): - def test_default_field(self): f = TimeRangeFilter() field = f.field self.assertIsInstance(field, TimeRangeField) def test_filtering_range(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=time(10, 15), stop=time(12, 30)) f = TimeRangeFilter() f.filter(qs, value) - qs.filter.assert_called_once_with( - None__range=(time(10, 15), time(12, 30))) + qs.filter.assert_called_once_with(None__range=(time(10, 15), time(12, 30))) def test_filtering_start(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=time(10, 15), stop=None) f = TimeRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=time(10, 15)) def test_filtering_stop(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=time(12, 30)) f = TimeRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=time(12, 30)) def test_filtering_skipped_with_none_value(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = TimeRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) @@ -1247,14 +1339,12 @@ def test_filtering_skipped_with_none_value(self): def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock(start=time(10, 15), stop=time(12, 30)) - f = TimeRangeFilter(lookup_expr='gte') + f = TimeRangeFilter(lookup_expr="gte") f.filter(qs, value) - qs.filter.assert_called_once_with( - None__range=(time(10, 15), time(12, 30))) + qs.filter.assert_called_once_with(None__range=(time(10, 15), time(12, 30))) class AllValuesFilterTests(TestCase): - def test_default_field_without_assigning_model(self): f = AllValuesFilter() with self.assertRaises(AttributeError): @@ -1262,9 +1352,14 @@ def test_default_field_without_assigning_model(self): def test_default_field_with_assigning_model(self): mocked = mock.Mock() - chained_call = '.'.join(['_default_manager', 'distinct.return_value', - 'order_by.return_value', - 'values_list.return_value']) + chained_call = ".".join( + [ + "_default_manager", + "distinct.return_value", + "order_by.return_value", + "values_list.return_value", + ] + ) mocked.configure_mock(**{chained_call: iter([])}) f = AllValuesFilter() f.model = mocked @@ -1272,86 +1367,99 @@ def test_default_field_with_assigning_model(self): self.assertIsInstance(field, forms.ChoiceField) def test_empty_value_in_choices(self): - f = AllValuesFilter(field_name='username') + f = AllValuesFilter(field_name="username") f.model = User - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ]) + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ], + ) class LookupChoiceFilterTests(TestCase): - def test_normalize_lookup_no_display_label(self): # display label has underscores replaced and is capitalized - display_label = LookupChoiceFilter.normalize_lookup('has_key') - self.assertEqual(display_label, ('has_key', 'Has key')) + display_label = LookupChoiceFilter.normalize_lookup("has_key") + self.assertEqual(display_label, ("has_key", "Has key")) def test_normalize_lookup_with_display_label(self): # display label is not transformed if provided - display_label = LookupChoiceFilter.normalize_lookup(('equal', 'equals')) - self.assertEqual(display_label, ('equal', 'equals')) + display_label = LookupChoiceFilter.normalize_lookup(("equal", "equals")) + self.assertEqual(display_label, ("equal", "equals")) def test_lookup_choices_default(self): # Lookup choices should default to the model field's registered lookups - f = LookupChoiceFilter(field_name='username', lookup_choices=None) + f = LookupChoiceFilter(field_name="username", lookup_choices=None) f.model = User choice_field = f.field.fields[1] self.assertEqual( len(choice_field.choices), - len(User._meta.get_field('username').get_lookups()) + 1 + len(User._meta.get_field("username").get_lookups()) + 1, ) field_choices = dict(choice_field.choices) - self.assertEqual(field_choices['exact'], 'Exact') - self.assertEqual(field_choices['startswith'], 'Startswith') + self.assertEqual(field_choices["exact"], "Exact") + self.assertEqual(field_choices["startswith"], "Startswith") def test_lookup_choices_list(self): - f = LookupChoiceFilter(field_name='username', lookup_choices=[ - 'exact', - 'startswith', - 'has_key' - ]) + f = LookupChoiceFilter( + field_name="username", lookup_choices=["exact", "startswith", "has_key"] + ) choice_field = f.field.fields[1] - self.assertEqual(list(choice_field.choices), [ - ('', '---------'), - ('exact', 'Exact'), - ('startswith', 'Startswith'), - ('has_key', 'Has key'), - ]) + self.assertEqual( + list(choice_field.choices), + [ + ("", "---------"), + ("exact", "Exact"), + ("startswith", "Startswith"), + ("has_key", "Has key"), + ], + ) def test_lookup_choices_pairs(self): - f = LookupChoiceFilter(field_name='username', lookup_choices=[ - ('exact', 'Is equal to'), - ('startswith', 'Starts with'), - ]) + f = LookupChoiceFilter( + field_name="username", + lookup_choices=[ + ("exact", "Is equal to"), + ("startswith", "Starts with"), + ], + ) choice_field = f.field.fields[1] - self.assertEqual(list(choice_field.choices), [ - ('', '---------'), - ('exact', 'Is equal to'), - ('startswith', 'Starts with'), - ]) + self.assertEqual( + list(choice_field.choices), + [ + ("", "---------"), + ("exact", "Is equal to"), + ("startswith", "Starts with"), + ], + ) def test_lookup_choices_empty_label_default(self): - f = LookupChoiceFilter(field_name='username', lookup_choices=[]) + f = LookupChoiceFilter(field_name="username", lookup_choices=[]) choice_field = f.field.fields[1] - self.assertEqual(list(choice_field.choices), [('', '---------')]) + self.assertEqual(list(choice_field.choices), [("", "---------")]) def test_lookup_choices_empty_label_disabled(self): - f = LookupChoiceFilter(field_name='username', empty_label=None, lookup_choices=[]) + f = LookupChoiceFilter( + field_name="username", empty_label=None, lookup_choices=[] + ) choice_field = f.field.fields[1] self.assertEqual(list(choice_field.choices), []) def test_filtering(self): - qs = mock.Mock(spec=['filter']) - f = LookupChoiceFilter(field_name='somefield', lookup_choices=['some_lookup_expr']) - result = f.filter(qs, Lookup('value', 'some_lookup_expr')) - qs.filter.assert_called_once_with(somefield__some_lookup_expr='value') + qs = mock.Mock(spec=["filter"]) + f = LookupChoiceFilter( + field_name="somefield", lookup_choices=["some_lookup_expr"] + ) + result = f.filter(qs, Lookup("value", "some_lookup_expr")) + qs.filter.assert_called_once_with(somefield__some_lookup_expr="value") self.assertNotEqual(qs, result) @@ -1363,8 +1471,8 @@ class NumberInFilter(BaseCSVFilter, NumberFilter): class DateTimeYearInFilter(BaseCSVFilter, DateTimeFilter): pass - self.number_in = NumberInFilter(lookup_expr='in') - self.datetimeyear_in = DateTimeYearInFilter(lookup_expr='year__in') + self.number_in = NumberInFilter(lookup_expr="in") + self.datetimeyear_in = DateTimeYearInFilter(lookup_expr="year__in") def test_default_field(self): f = BaseCSVFilter() @@ -1375,21 +1483,21 @@ def test_concrete_field(self): field = self.number_in.field self.assertIsInstance(field, forms.DecimalField) self.assertIsInstance(field, BaseCSVField) - self.assertEqual(field.__class__.__name__, 'DecimalInField') + self.assertEqual(field.__class__.__name__, "DecimalInField") field = self.datetimeyear_in.field self.assertIsInstance(field, forms.DateTimeField) self.assertIsInstance(field, BaseCSVField) - self.assertEqual(field.__class__.__name__, 'DateTimeYearInField') + self.assertEqual(field.__class__.__name__, "DateTimeYearInField") def test_filtering(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = self.number_in f.filter(qs, [1, 2]) qs.filter.assert_called_once_with(None__in=[1, 2]) def test_filtering_skipped_with_none_value(self): - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = self.number_in result = f.filter(qs, None) self.assertEqual(qs, result) @@ -1406,7 +1514,7 @@ def test_filtering(self): class NumberInFilter(BaseInFilter, NumberFilter): pass - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = NumberInFilter() f.filter(qs, [1, 2]) qs.filter.assert_called_once_with(None__in=[1, 2]) @@ -1417,7 +1525,7 @@ def test_filtering(self): class NumberInFilter(BaseRangeFilter, NumberFilter): pass - qs = mock.Mock(spec=['filter']) + qs = mock.Mock(spec=["filter"]) f = NumberInFilter() f.filter(qs, [1, 2]) qs.filter.assert_called_once_with(None__range=[1, 2]) @@ -1430,25 +1538,25 @@ def test_default_field(self): self.assertIsInstance(field, forms.ChoiceField) def test_filtering(self): - qs = mock.Mock(spec=['order_by']) + qs = mock.Mock(spec=["order_by"]) f = OrderingFilter() - f.filter(qs, ['a', 'b']) - qs.order_by.assert_called_once_with('a', 'b') + f.filter(qs, ["a", "b"]) + qs.order_by.assert_called_once_with("a", "b") def test_filtering_descending(self): - qs = mock.Mock(spec=['order_by']) + qs = mock.Mock(spec=["order_by"]) f = OrderingFilter() - f.filter(qs, ['-a']) - qs.order_by.assert_called_once_with('-a') + f.filter(qs, ["-a"]) + qs.order_by.assert_called_once_with("-a") def test_filtering_with_fields(self): - qs = mock.Mock(spec=['order_by']) - f = OrderingFilter(fields={'a': 'b'}) - f.filter(qs, ['b', '-b']) - qs.order_by.assert_called_once_with('a', '-a') + qs = mock.Mock(spec=["order_by"]) + f = OrderingFilter(fields={"a": "b"}) + f.filter(qs, ["b", "-b"]) + qs.order_by.assert_called_once_with("a", "-a") def test_filtering_skipped_with_none_value(self): - qs = mock.Mock(spec=['order_by']) + qs = mock.Mock(spec=["order_by"]) f = OrderingFilter() result = f.filter(qs, None) self.assertEqual(qs, result) @@ -1456,90 +1564,102 @@ def test_filtering_skipped_with_none_value(self): def test_choices_unaltered(self): # provided 'choices' should not be altered when 'fields' is present f = OrderingFilter( - choices=(('a', 'A'), ('b', 'B')), - fields=(('a', 'c'), ('b', 'd')), + choices=(("a", "A"), ("b", "B")), + fields=(("a", "c"), ("b", "d")), ) - self.assertSequenceEqual(list(f.field.choices), ( - ('', '---------'), - ('a', 'A'), - ('b', 'B'), - )) + self.assertSequenceEqual( + list(f.field.choices), + ( + ("", "---------"), + ("a", "A"), + ("b", "B"), + ), + ) def test_choices_from_fields(self): f = OrderingFilter( - fields=(('a', 'c'), ('b', 'd')), + fields=(("a", "c"), ("b", "d")), ) - self.assertSequenceEqual(list(f.field.choices), ( - ('', '---------'), - ('c', 'C'), - ('-c', 'C (descending)'), - ('d', 'D'), - ('-d', 'D (descending)'), - )) + self.assertSequenceEqual( + list(f.field.choices), + ( + ("", "---------"), + ("c", "C"), + ("-c", "C (descending)"), + ("d", "D"), + ("-d", "D (descending)"), + ), + ) def test_field_labels(self): f = OrderingFilter( - fields=(('a', 'c'), ('b', 'd')), - field_labels={'a': 'foo'}, + fields=(("a", "c"), ("b", "d")), + field_labels={"a": "foo"}, ) - self.assertSequenceEqual(list(f.field.choices), ( - ('', '---------'), - ('c', 'foo'), - ('-c', 'foo (descending)'), - ('d', 'D'), - ('-d', 'D (descending)'), - )) + self.assertSequenceEqual( + list(f.field.choices), + ( + ("", "---------"), + ("c", "foo"), + ("-c", "foo (descending)"), + ("d", "D"), + ("-d", "D (descending)"), + ), + ) def test_field_labels_descending(self): f = OrderingFilter( - fields=['username'], + fields=["username"], field_labels={ - 'username': 'BLABLA', - '-username': 'XYZXYZ', - } + "username": "BLABLA", + "-username": "XYZXYZ", + }, ) - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('username', 'BLABLA'), - ('-username', 'XYZXYZ'), - ]) + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("username", "BLABLA"), + ("-username", "XYZXYZ"), + ], + ) def test_normalize_fields(self): f = OrderingFilter.normalize_fields O = OrderedDict # noqa - self.assertIn('a', f({'a': 'b'})) + self.assertIn("a", f({"a": "b"})) - self.assertEqual( - f(O([('a', 'b'), ('c', 'd')])), - O([('a', 'b'), ('c', 'd')]) - ) + self.assertEqual(f(O([("a", "b"), ("c", "d")])), O([("a", "b"), ("c", "d")])) - self.assertEqual( - f([('a', 'b'), ('c', 'd')]), - O([('a', 'b'), ('c', 'd')]) - ) + self.assertEqual(f([("a", "b"), ("c", "d")]), O([("a", "b"), ("c", "d")])) - self.assertEqual( - f(['a', 'b']), - O([('a', 'a'), ('b', 'b')]) - ) + self.assertEqual(f(["a", "b"]), O([("a", "a"), ("b", "b")])) with self.assertRaises(AssertionError) as ctx: f(None) - self.assertEqual(str(ctx.exception), "'fields' must be an iterable (e.g., a list, tuple, or mapping).") + self.assertEqual( + str(ctx.exception), + "'fields' must be an iterable (e.g., a list, tuple, or mapping).", + ) with self.assertRaises(AssertionError) as ctx: - f([('a', 'b', 'c')]) - self.assertEqual(str(ctx.exception), "'fields' must contain strings or (field name, param name) pairs.") + f([("a", "b", "c")]) + self.assertEqual( + str(ctx.exception), + "'fields' must contain strings or (field name, param name) pairs.", + ) with self.assertRaises(AssertionError) as ctx: f([0, 1, 2]) - self.assertEqual(str(ctx.exception), "'fields' must contain strings or (field name, param name) pairs.") + self.assertEqual( + str(ctx.exception), + "'fields' must contain strings or (field name, param name) pairs.", + ) def test_widget(self): f = OrderingFilter() @@ -1549,34 +1669,42 @@ def test_widget(self): self.assertIsInstance(widget, forms.Select) def test_translation_sanity(self): - with translation.override('pl'): - self.assertEqual(_('Username'), 'Nazwa użytkownika') - self.assertEqual(_('%s (descending)') % _('Username'), 'Nazwa użytkownika (malejąco)') + with translation.override("pl"): + self.assertEqual(_("Username"), "Nazwa użytkownika") + self.assertEqual( + _("%s (descending)") % _("Username"), "Nazwa użytkownika (malejąco)" + ) def test_translation_default_label(self): - with translation.override('pl'): - f = OrderingFilter(fields=['username']) + with translation.override("pl"): + f = OrderingFilter(fields=["username"]) - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('username', 'Nazwa użytkownika'), - ('-username', 'Nazwa użytkownika (malejąco)'), - ]) + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("username", "Nazwa użytkownika"), + ("-username", "Nazwa użytkownika (malejąco)"), + ], + ) def test_translation_override_label(self): - with translation.override('pl'): + with translation.override("pl"): f = OrderingFilter( - fields=['username'], - field_labels={'username': 'BLABLA'}, + fields=["username"], + field_labels={"username": "BLABLA"}, ) - self.assertEqual(list(f.field.choices), [ - ('', '---------'), - ('username', 'BLABLA'), - ('-username', 'BLABLA (malejąco)'), - ]) + self.assertEqual( + list(f.field.choices), + [ + ("", "---------"), + ("username", "BLABLA"), + ("-username", "BLABLA (malejąco)"), + ], + ) def test_help_text(self): # regression test for #756 - the usual CSV help_text is not relevant to ordering filters. - self.assertEqual(OrderingFilter().field.help_text, '') - self.assertEqual(OrderingFilter(help_text='a').field.help_text, 'a') + self.assertEqual(OrderingFilter().field.help_text, "") + self.assertEqual(OrderingFilter(help_text="a").field.help_text, "a") diff --git a/tests/test_filterset.py b/tests/test_filterset.py index d3cd949cd..e8da1469d 100644 --- a/tests/test_filterset.py +++ b/tests/test_filterset.py @@ -1,5 +1,5 @@ -import mock import unittest +from unittest import mock from django.db import models from django.test import TestCase, override_settings @@ -17,7 +17,7 @@ ModelChoiceFilter, ModelMultipleChoiceFilter, NumberFilter, - UUIDFilter + UUIDFilter, ) from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS, FilterSet from django_filters.widgets import BooleanWidget @@ -38,28 +38,26 @@ SubnetMaskField, User, UUIDTestModel, - Worker + Worker, ) from .utils import MockQuerySet class HelperMethodsTests(TestCase): - - @unittest.skip('todo') + @unittest.skip("todo") def test_get_declared_filters(self): pass - @unittest.skip('todo') + @unittest.skip("todo") def test_filters_for_model(self): pass - @unittest.skip('todo') + @unittest.skip("todo") def test_filterset_factory(self): pass class DbFieldDefaultFiltersTests(TestCase): - def test_expected_db_fields_get_filters(self): to_check = [ models.BooleanField, @@ -106,214 +104,199 @@ def test_expected_db_fields_do_not_get_filters(self): class FilterSetFilterForFieldTests(TestCase): - def test_filter_found_for_field(self): - f = User._meta.get_field('username') - result = FilterSet.filter_for_field(f, 'username') + f = User._meta.get_field("username") + result = FilterSet.filter_for_field(f, "username") self.assertIsInstance(result, CharFilter) - self.assertEqual(result.field_name, 'username') + self.assertEqual(result.field_name, "username") def test_filter_found_for_uuidfield(self): - f = UUIDTestModel._meta.get_field('uuid') - result = FilterSet.filter_for_field(f, 'uuid') + f = UUIDTestModel._meta.get_field("uuid") + result = FilterSet.filter_for_field(f, "uuid") self.assertIsInstance(result, UUIDFilter) - self.assertEqual(result.field_name, 'uuid') + self.assertEqual(result.field_name, "uuid") def test_filter_found_for_autofield(self): - f = User._meta.get_field('id') - result = FilterSet.filter_for_field(f, 'id') + f = User._meta.get_field("id") + result = FilterSet.filter_for_field(f, "id") self.assertIsInstance(result, NumberFilter) - self.assertEqual(result.field_name, 'id') + self.assertEqual(result.field_name, "id") def test_field_with_extras(self): - f = User._meta.get_field('favorite_books') - result = FilterSet.filter_for_field(f, 'favorite_books') + f = User._meta.get_field("favorite_books") + result = FilterSet.filter_for_field(f, "favorite_books") self.assertIsInstance(result, ModelMultipleChoiceFilter) - self.assertEqual(result.field_name, 'favorite_books') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, Book) + self.assertEqual(result.field_name, "favorite_books") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, Book) def test_field_with_choices(self): - f = User._meta.get_field('status') - result = FilterSet.filter_for_field(f, 'status') + f = User._meta.get_field("status") + result = FilterSet.filter_for_field(f, "status") self.assertIsInstance(result, ChoiceFilter) - self.assertEqual(result.field_name, 'status') - self.assertTrue('choices' in result.extra) - self.assertIsNotNone(result.extra['choices']) + self.assertEqual(result.field_name, "status") + self.assertTrue("choices" in result.extra) + self.assertIsNotNone(result.extra["choices"]) def test_field_that_is_subclassed(self): - f = User._meta.get_field('first_name') - result = FilterSet.filter_for_field(f, 'first_name') + f = User._meta.get_field("first_name") + result = FilterSet.filter_for_field(f, "first_name") self.assertIsInstance(result, CharFilter) def test_unknown_field_type_error(self): - f = NetworkSetting._meta.get_field('mask') + f = NetworkSetting._meta.get_field("mask") with self.assertRaises(AssertionError) as excinfo: - FilterSet.filter_for_field(f, 'mask') + FilterSet.filter_for_field(f, "mask") self.assertIn( "FilterSet resolved field 'mask' with 'exact' lookup " "to an unrecognized field type SubnetMaskField", - excinfo.exception.args[0]) + excinfo.exception.args[0], + ) def test_symmetrical_selfref_m2m_field(self): - f = Node._meta.get_field('adjacents') - result = FilterSet.filter_for_field(f, 'adjacents') + f = Node._meta.get_field("adjacents") + result = FilterSet.filter_for_field(f, "adjacents") self.assertIsInstance(result, ModelMultipleChoiceFilter) - self.assertEqual(result.field_name, 'adjacents') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, Node) + self.assertEqual(result.field_name, "adjacents") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, Node) def test_non_symmetrical_selfref_m2m_field(self): - f = DirectedNode._meta.get_field('outbound_nodes') - result = FilterSet.filter_for_field(f, 'outbound_nodes') + f = DirectedNode._meta.get_field("outbound_nodes") + result = FilterSet.filter_for_field(f, "outbound_nodes") self.assertIsInstance(result, ModelMultipleChoiceFilter) - self.assertEqual(result.field_name, 'outbound_nodes') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, DirectedNode) + self.assertEqual(result.field_name, "outbound_nodes") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, DirectedNode) def test_m2m_field_with_through_model(self): - f = Business._meta.get_field('employees') - result = FilterSet.filter_for_field(f, 'employees') + f = Business._meta.get_field("employees") + result = FilterSet.filter_for_field(f, "employees") self.assertIsInstance(result, ModelMultipleChoiceFilter) - self.assertEqual(result.field_name, 'employees') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, Worker) + self.assertEqual(result.field_name, "employees") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, Worker) def test_transformed_lookup_expr(self): - f = Comment._meta.get_field('date') - result = FilterSet.filter_for_field(f, 'date', 'year__gte') + f = Comment._meta.get_field("date") + result = FilterSet.filter_for_field(f, "date", "year__gte") self.assertIsInstance(result, NumberFilter) - self.assertEqual(result.field_name, 'date') + self.assertEqual(result.field_name, "date") - @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains') + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="icontains") def test_modified_default_lookup(self): - f = User._meta.get_field('username') - result = FilterSet.filter_for_field(f, 'username') + f = User._meta.get_field("username") + result = FilterSet.filter_for_field(f, "username") self.assertIsInstance(result, CharFilter) - self.assertEqual(result.lookup_expr, 'icontains') + self.assertEqual(result.lookup_expr, "icontains") - @unittest.skip('todo') + @unittest.skip("todo") def test_filter_overrides(self): pass class FilterSetFilterForLookupTests(TestCase): - def test_filter_for_ISNULL_lookup(self): - f = Article._meta.get_field('author') - result, params = FilterSet.filter_for_lookup(f, 'isnull') + f = Article._meta.get_field("author") + result, params = FilterSet.filter_for_lookup(f, "isnull") self.assertEqual(result, BooleanFilter) self.assertDictEqual(params, {}) def test_filter_for_IN_lookup(self): - f = Article._meta.get_field('author') - result, params = FilterSet.filter_for_lookup(f, 'in') + f = Article._meta.get_field("author") + result, params = FilterSet.filter_for_lookup(f, "in") self.assertTrue(issubclass(result, ModelChoiceFilter)) self.assertTrue(issubclass(result, BaseInFilter)) - self.assertEqual(params['to_field_name'], 'id') + self.assertEqual(params["to_field_name"], "id") def test_filter_for_RANGE_lookup(self): - f = Article._meta.get_field('author') - result, params = FilterSet.filter_for_lookup(f, 'range') + f = Article._meta.get_field("author") + result, params = FilterSet.filter_for_lookup(f, "range") self.assertTrue(issubclass(result, ModelChoiceFilter)) self.assertTrue(issubclass(result, BaseRangeFilter)) - self.assertEqual(params['to_field_name'], 'id') + self.assertEqual(params["to_field_name"], "id") def test_isnull_with_filter_overrides(self): class OFilterSet(FilterSet): class Meta: filter_overrides = { models.BooleanField: { - 'filter_class': BooleanFilter, - 'extra': lambda f: { - 'widget': BooleanWidget, + "filter_class": BooleanFilter, + "extra": lambda f: { + "widget": BooleanWidget, }, }, } - f = Article._meta.get_field('author') - result, params = OFilterSet.filter_for_lookup(f, 'isnull') + f = Article._meta.get_field("author") + result, params = OFilterSet.filter_for_lookup(f, "isnull") self.assertEqual(result, BooleanFilter) - self.assertEqual(params['widget'], BooleanWidget) + self.assertEqual(params["widget"], BooleanWidget) class ReverseFilterSetFilterForFieldTests(TestCase): # Test reverse relationships for `filter_for_field` def test_reverse_o2o_relationship(self): - f = Account._meta.get_field('profile') - result = FilterSet.filter_for_field(f, 'profile') + f = Account._meta.get_field("profile") + result = FilterSet.filter_for_field(f, "profile") self.assertIsInstance(result, ModelChoiceFilter) - self.assertEqual(result.field_name, 'profile') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, Profile) + self.assertEqual(result.field_name, "profile") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, Profile) def test_reverse_fk_relationship(self): - f = User._meta.get_field('comments') - result = FilterSet.filter_for_field(f, 'comments') + f = User._meta.get_field("comments") + result = FilterSet.filter_for_field(f, "comments") self.assertIsInstance(result, ModelMultipleChoiceFilter) - self.assertEqual(result.field_name, 'comments') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, Comment) + self.assertEqual(result.field_name, "comments") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, Comment) def test_reverse_m2m_relationship(self): - f = Book._meta.get_field('lovers') - result = FilterSet.filter_for_field(f, 'lovers') + f = Book._meta.get_field("lovers") + result = FilterSet.filter_for_field(f, "lovers") self.assertIsInstance(result, ModelMultipleChoiceFilter) - self.assertEqual(result.field_name, 'lovers') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, User) + self.assertEqual(result.field_name, "lovers") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, User) def test_reverse_non_symmetrical_selfref_m2m_field(self): - f = DirectedNode._meta.get_field('inbound_nodes') - result = FilterSet.filter_for_field(f, 'inbound_nodes') + f = DirectedNode._meta.get_field("inbound_nodes") + result = FilterSet.filter_for_field(f, "inbound_nodes") self.assertIsInstance(result, ModelMultipleChoiceFilter) - self.assertEqual(result.field_name, 'inbound_nodes') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, DirectedNode) + self.assertEqual(result.field_name, "inbound_nodes") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, DirectedNode) def test_reverse_m2m_field_with_through_model(self): - f = Worker._meta.get_field('employers') - result = FilterSet.filter_for_field(f, 'employers') + f = Worker._meta.get_field("employers") + result = FilterSet.filter_for_field(f, "employers") self.assertIsInstance(result, ModelMultipleChoiceFilter) - self.assertEqual(result.field_name, 'employers') - self.assertTrue('queryset' in result.extra) - self.assertIsNotNone(result.extra['queryset']) - self.assertEqual(result.extra['queryset'].model, Business) + self.assertEqual(result.field_name, "employers") + self.assertTrue("queryset" in result.extra) + self.assertIsNotNone(result.extra["queryset"]) + self.assertEqual(result.extra["queryset"].model, Business) def test_reverse_relationship_lookup_expr(self): - f = Book._meta.get_field('lovers') - result = FilterSet.filter_for_field(f, 'lovers', 'isnull') + f = Book._meta.get_field("lovers") + result = FilterSet.filter_for_field(f, "lovers", "isnull") self.assertIsInstance(result, BooleanFilter) - self.assertEqual(result.field_name, 'lovers') - self.assertEqual(result.lookup_expr, 'isnull') - - -class FilterSetFilterForReverseFieldTests(TestCase): - - def test_method_raises_assertion(self): - msg = ("`F.filter_for_reverse_field` has been removed. " - "`F.filter_for_field` now generates filters for reverse fields.") - - with self.assertRaisesMessage(AssertionError, msg): - class F(FilterSet): - @classmethod - def filter_for_reverse_field(cls, field, field_name): - pass + self.assertEqual(result.field_name, "lovers") + self.assertEqual(result.lookup_expr, "isnull") class FilterSetClassCreationTests(TestCase): - def test_no_filters(self): class F(FilterSet): pass @@ -326,47 +309,47 @@ class F(FilterSet): username = CharFilter() self.assertEqual(len(F.declared_filters), 1) - self.assertListEqual(list(F.declared_filters), ['username']) + self.assertListEqual(list(F.declared_filters), ["username"]) self.assertEqual(len(F.base_filters), 1) - self.assertListEqual(list(F.base_filters), ['username']) + self.assertListEqual(list(F.base_filters), ["username"]) - @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains') + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="icontains") def test_declaring_filter_other_default_lookup(self): class F(FilterSet): username = CharFilter() - self.assertEqual(F.base_filters['username'].lookup_expr, 'icontains') + self.assertEqual(F.base_filters["username"].lookup_expr, "icontains") def test_model_derived(self): class F(FilterSet): class Meta: model = Book - fields = '__all__' + fields = "__all__" self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 3) - self.assertListEqual(list(F.base_filters), - ['title', 'price', 'average_rating']) + self.assertListEqual(list(F.base_filters), ["title", "price", "average_rating"]) - @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='icontains') + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="icontains") def test_model_derived_other_default_lookup(self): class F(FilterSet): class Meta: model = Book - fields = '__all__' + fields = "__all__" for filter_ in F.base_filters.values(): - self.assertEqual(filter_.lookup_expr, 'icontains') + self.assertEqual(filter_.lookup_expr, "icontains") def test_model_no_fields_or_exclude(self): with self.assertRaises(AssertionError) as excinfo: + class F(FilterSet): class Meta: model = Book self.assertIn( "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude'", - str(excinfo.exception) + str(excinfo.exception), ) def test_model_fields_empty(self): @@ -388,8 +371,7 @@ class Meta: self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 3) - self.assertListEqual(list(F.base_filters), - ['title', 'price', 'average_rating']) + self.assertListEqual(list(F.base_filters), ["title", "price", "average_rating"]) def test_declared_and_model_derived(self): class F(FilterSet): @@ -397,12 +379,13 @@ class F(FilterSet): class Meta: model = Book - fields = '__all__' + fields = "__all__" self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 4) - self.assertListEqual(list(F.base_filters), - ['title', 'price', 'average_rating', 'username']) + self.assertListEqual( + list(F.base_filters), ["title", "price", "average_rating", "username"] + ) def test_meta_fields_with_declared_and_model_derived(self): class F(FilterSet): @@ -410,36 +393,47 @@ class F(FilterSet): class Meta: model = Book - fields = ('username', 'price') + fields = ("username", "price") self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 2) - self.assertListEqual(list(F.base_filters), ['username', 'price']) + self.assertListEqual(list(F.base_filters), ["username", "price"]) def test_meta_fields_dictionary_derived(self): class F(FilterSet): class Meta: model = Book - fields = {'price': ['exact', 'gte', 'lte'], } + fields = { + "price": ["exact", "gte", "lte"], + } self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 3) - expected_list = ['price', 'price__gte', 'price__lte', ] + expected_list = [ + "price", + "price__gte", + "price__lte", + ] self.assertCountEqual(list(F.base_filters), expected_list) - @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='lte') + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="lte") def test_meta_fields_dictionary_derived_other_default_lookup(self): class F(FilterSet): - class Meta: model = Book - fields = {'price': ['exact', 'gte', 'lte'], } + fields = { + "price": ["exact", "gte", "lte"], + } self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 3) - expected_list = ['price__exact', 'price__gte', 'price', ] + expected_list = [ + "price__exact", + "price__gte", + "price", + ] self.assertCountEqual(list(F.base_filters), expected_list) def test_meta_fields_containing_autofield(self): @@ -448,11 +442,11 @@ class F(FilterSet): class Meta: model = Book - fields = ('id', 'username', 'price') + fields = ("id", "username", "price") self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 3) - self.assertListEqual(list(F.base_filters), ['id', 'username', 'price']) + self.assertListEqual(list(F.base_filters), ["id", "username", "price"]) def test_meta_fields_dictionary_autofield(self): class F(FilterSet): @@ -461,40 +455,40 @@ class F(FilterSet): class Meta: model = User fields = { - 'id': ['exact'], - 'username': ['exact'], + "id": ["exact"], + "username": ["exact"], } self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 2) - expected_list = ['id', 'username'] + expected_list = ["id", "username"] self.assertCountEqual(list(F.base_filters), expected_list) def test_meta_fields_list_containing_unknown_fields(self): - msg = ("'Meta.fields' must not contain non-model field names: " - "other, another") + msg = "'Meta.fields' must not contain non-model field names: " "other, another" with self.assertRaisesMessage(TypeError, msg): + class F(FilterSet): username = CharFilter() class Meta: model = Book - fields = ('username', 'price', 'other', 'another') + fields = ("username", "price", "other", "another") def test_meta_fields_dict_containing_unknown_fields(self): msg = "'Meta.fields' must not contain non-model field names: other" with self.assertRaisesMessage(TypeError, msg): - class F(FilterSet): + class F(FilterSet): class Meta: model = Book fields = { - 'id': ['exact'], - 'title': ['exact'], - 'other': ['exact'], + "id": ["exact"], + "title": ["exact"], + "other": ["exact"], } def test_meta_fields_dict_containing_declarative_alias(self): @@ -502,27 +496,29 @@ def test_meta_fields_dict_containing_declarative_alias(self): msg = "'Meta.fields' must not contain non-model field names: other" with self.assertRaisesMessage(TypeError, msg): + class F(FilterSet): other = CharFilter() class Meta: model = Book fields = { - 'id': ['exact'], - 'title': ['exact'], - 'other': ['exact'], + "id": ["exact"], + "title": ["exact"], + "other": ["exact"], } def test_meta_fields_invalid_lookup(self): # We want to ensure that non existent lookups (or just simple misspellings) - # throw a useful exception containg the field and lookup expr. + # throw a useful exception containing the field and lookup expr. msg = "Unsupported lookup 'flub' for field 'tests.User.username'." with self.assertRaisesMessage(FieldLookupError, msg): + class F(FilterSet): class Meta: model = User - fields = {'username': ['flub']} + fields = {"username": ["flub"]} def test_meta_exlude_with_declared_and_declared_wins(self): class F(FilterSet): @@ -530,12 +526,13 @@ class F(FilterSet): class Meta: model = Book - exclude = ('username', 'price') + exclude = ("username", "price") self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 3) - self.assertListEqual(list(F.base_filters), - ['title', 'average_rating', 'username']) + self.assertListEqual( + list(F.base_filters), ["title", "average_rating", "username"] + ) def test_meta_fields_and_exlude_and_exclude_wins(self): class F(FilterSet): @@ -543,33 +540,32 @@ class F(FilterSet): class Meta: model = Book - fields = ('username', 'title', 'price') - exclude = ('title',) + fields = ("username", "title", "price") + exclude = ("title",) self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 2) - self.assertListEqual(list(F.base_filters), - ['username', 'price']) + self.assertListEqual(list(F.base_filters), ["username", "price"]) def test_meta_exlude_with_no_fields(self): class F(FilterSet): class Meta: model = Book - exclude = ('price', ) + exclude = ("price",) self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 2) - self.assertListEqual(list(F.base_filters), - ['title', 'average_rating']) + self.assertListEqual(list(F.base_filters), ["title", "average_rating"]) def test_filterset_class_inheritance(self): class F(FilterSet): class Meta: model = Book - fields = '__all__' + fields = "__all__" class G(F): pass + self.assertEqual(set(F.base_filters), set(G.base_filters)) class F(FilterSet): @@ -577,38 +573,37 @@ class F(FilterSet): class Meta: model = Book - fields = '__all__' + fields = "__all__" class G(F): pass + self.assertEqual(set(F.base_filters), set(G.base_filters)) def test_abstract_model_inheritance(self): class F(FilterSet): class Meta: model = Restaurant - fields = '__all__' + fields = "__all__" - self.assertEqual(set(F.base_filters), set(['name', 'serves_pizza'])) + self.assertEqual(set(F.base_filters), set(["name", "serves_pizza"])) class F(FilterSet): class Meta: model = Restaurant - fields = ['name', 'serves_pizza'] + fields = ["name", "serves_pizza"] - self.assertEqual(set(F.base_filters), set(['name', 'serves_pizza'])) + self.assertEqual(set(F.base_filters), set(["name", "serves_pizza"])) def test_custom_field_gets_filter_from_override(self): class F(FilterSet): class Meta: model = NetworkSetting - fields = '__all__' + fields = "__all__" - filter_overrides = { - SubnetMaskField: {'filter_class': CharFilter} - } + filter_overrides = {SubnetMaskField: {"filter_class": CharFilter}} - self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask', 'cidr']) + self.assertEqual(list(F.base_filters.keys()), ["ip", "mask", "cidr"]) def test_custom_declared_field_no_warning(self): class F(FilterSet): @@ -616,20 +611,20 @@ class F(FilterSet): class Meta: model = NetworkSetting - fields = ['mask'] + fields = ["mask"] - self.assertEqual(list(F.base_filters.keys()), ['mask']) + self.assertEqual(list(F.base_filters.keys()), ["mask"]) def test_filterset_for_proxy_model(self): class F(FilterSet): class Meta: model = User - fields = '__all__' + fields = "__all__" class ProxyF(FilterSet): class Meta: model = AdminUser - fields = '__all__' + fields = "__all__" self.assertEqual(list(F.base_filters), list(ProxyF.base_filters)) @@ -637,17 +632,17 @@ def test_filterset_for_mti_model(self): class F(FilterSet): class Meta: model = Account - fields = '__all__' + fields = "__all__" class FtiF(FilterSet): class Meta: model = BankAccount - fields = '__all__' + fields = "__all__" # fails due to 'account_ptr' getting picked up self.assertEqual( - list(F.base_filters) + ['amount_saved'], - list(FtiF.base_filters)) + list(F.base_filters) + ["amount_saved"], list(FtiF.base_filters) + ) def test_declared_filter_disabling(self): class Parent(FilterSet): @@ -664,18 +659,18 @@ class Grandchild(Child): self.assertEqual(len(Child.base_filters), 1) self.assertEqual(len(Grandchild.base_filters), 1) - @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR='lt') + @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="lt") def test_transforms_other_default_lookup(self): class F(FilterSet): class Meta: model = Article fields = { - 'published': ['lt', 'year__lt'], + "published": ["lt", "year__lt"], } self.assertEqual(len(F.base_filters), 2) - expected_list = ['published', 'published__year'] + expected_list = ["published", "published__year"] self.assertCountEqual(list(F.base_filters), expected_list) def test_declared_filter_multiple_inheritance(self): @@ -689,7 +684,7 @@ class F(A, B): pass filters = {name: type(f) for name, f in F.declared_filters.items()} - self.assertEqual(filters, {'f': CharFilter}) + self.assertEqual(filters, {"f": CharFilter}) def test_declared_filter_multiple_inheritance_field_ordering(self): class Base(FilterSet): @@ -713,20 +708,19 @@ class F(A, B): # - `F.f2` should override `Base.F2` # - `A.f3` should override `B.f3` assert fields == { - 'f1': CharFilter, - 'f2': NumberFilter, - 'f3': NumberFilter, - 'f4': CharFilter, - 'f5': CharFilter, + "f1": CharFilter, + "f2": NumberFilter, + "f3": NumberFilter, + "f4": CharFilter, + "f5": CharFilter, } class FilterSetInstantiationTests(TestCase): - class F(FilterSet): class Meta: model = User - fields = ['username'] + fields = ["username"] def test_creating_instance(self): f = self.F() @@ -735,12 +729,11 @@ def test_creating_instance(self): self.assertEqual(len(f.filters), len(self.F.base_filters)) for name, filter_ in f.filters.items(): self.assertEqual( - filter_.model, - User, - "%s does not have model set correctly" % name) + filter_.model, User, "%s does not have model set correctly" % name + ) def test_creating_bound_instance(self): - f = self.F({'username': 'username'}) + f = self.F({"username": "username"}) self.assertTrue(f.is_bound) def test_creating_with_queryset(self): @@ -755,20 +748,18 @@ def test_creating_with_request(self): class FilterSetQuerysetTests(TestCase): - class F(FilterSet): invalid = CharFilter(method=lambda *args: None) class Meta: model = User - fields = ['username', 'invalid'] + fields = ["username", "invalid"] def test_filter_queryset_called_once(self): m = MockQuerySet() - f = self.F({'username': 'bob'}, queryset=m) + f = self.F({"username": "bob"}, queryset=m) - with mock.patch.object(f, 'filter_queryset', - wraps=f.filter_queryset) as fn: + with mock.patch.object(f, "filter_queryset", wraps=f.filter_queryset) as fn: f.qs fn.assert_called_once_with(m.all()) f.qs @@ -777,8 +768,7 @@ def test_filter_queryset_called_once(self): def test_get_form_class_called_once(self): f = self.F() - with mock.patch.object(f, 'get_form_class', - wraps=f.get_form_class) as fn: + with mock.patch.object(f, "get_form_class", wraps=f.get_form_class) as fn: f.form fn.assert_called_once() f.form @@ -798,17 +788,16 @@ def test_form_caching(self): def test_qs_triggers_form_validation(self): m = MockQuerySet() - f = self.F({'username': 'bob'}, queryset=m) + f = self.F({"username": "bob"}, queryset=m) - with mock.patch.object(f.form, 'full_clean', - wraps=f.form.full_clean) as fn: + with mock.patch.object(f.form, "full_clean", wraps=f.form.full_clean) as fn: fn.assert_not_called() f.qs fn.assert_called() def test_filters_must_return_queryset(self): m = MockQuerySet() - f = self.F({'invalid': 'result'}, queryset=m) + f = self.F({"invalid": "result"}, queryset=m) msg = "Expected 'F.invalid' to return a QuerySet, but got a NoneType instead." with self.assertRaisesMessage(AssertionError, msg): @@ -817,7 +806,6 @@ def test_filters_must_return_queryset(self): # test filter.method here, as it depends on its parent FilterSet class FilterMethodTests(TestCase): - def test_none(self): # use a mock to bypass bound/unbound method equality class TestFilter(Filter): @@ -831,15 +819,15 @@ class TestFilter(Filter): def test_method_name(self): class F(FilterSet): - f = Filter(method='filter_f') + f = Filter(method="filter_f") def filter_f(self, qs, name, value): pass f = F({}, queryset=User.objects.all()) - self.assertEqual(f.filters['f'].method, 'filter_f') - self.assertEqual(f.filters['f'].filter.method, f.filter_f) - self.assertIsInstance(f.filters['f'].filter, FilterMethod) + self.assertEqual(f.filters["f"].method, "filter_f") + self.assertEqual(f.filters["f"].filter.method, f.filter_f) + self.assertIsInstance(f.filters["f"].filter, FilterMethod) def test_method_callable(self): def filter_f(qs, name, value): @@ -849,13 +837,13 @@ class F(FilterSet): f = Filter(method=filter_f) f = F({}, queryset=User.objects.all()) - self.assertEqual(f.filters['f'].method, filter_f) - self.assertEqual(f.filters['f'].filter.method, filter_f) - self.assertIsInstance(f.filters['f'].filter, FilterMethod) + self.assertEqual(f.filters["f"].method, filter_f) + self.assertEqual(f.filters["f"].filter.method, filter_f) + self.assertIsInstance(f.filters["f"].filter, FilterMethod) def test_request_available_during_method_called(self): class F(FilterSet): - f = Filter(method='filter_f') + f = Filter(method="filter_f") def filter_f(self, qs, name, value): # call mock request object to prove self.request can be accessed @@ -864,30 +852,30 @@ def filter_f(self, qs, name, value): m = mock.Mock() f = F({}, queryset=User.objects.all(), request=m) # call the filter - f.filters['f'].filter.method(User.objects.all(), 'f', '') + f.filters["f"].filter.method(User.objects.all(), "f", "") m.assert_called_once_with() def test_method_with_overridden_filter(self): # Some filter classes override the base filter() method. We need # to ensure that passing a method argument still works correctly class F(FilterSet): - f = DateRangeFilter(method='filter_f') + f = DateRangeFilter(method="filter_f") def filter_f(self, qs, name, value): pass f = F({}, queryset=User.objects.all()) - self.assertEqual(f.filters['f'].method, 'filter_f') - self.assertEqual(f.filters['f'].filter.method, f.filter_f) + self.assertEqual(f.filters["f"].method, "filter_f") + self.assertEqual(f.filters["f"].filter.method, f.filter_f) def test_parent_unresolvable(self): - f = Filter(method='filter_f') + f = Filter(method="filter_f") with self.assertRaises(AssertionError) as w: f.filter(User.objects.all(), 0) self.assertIn("'None'", str(w.exception)) - self.assertIn('parent', str(w.exception)) - self.assertIn('filter_f', str(w.exception)) + self.assertIn("parent", str(w.exception)) + self.assertIn("filter_f", str(w.exception)) def test_method_self_is_parent(self): # Ensure the method isn't 're-parented' on the `FilterMethod` helper class. @@ -895,7 +883,7 @@ def test_method_self_is_parent(self): request = MockQuerySet() class F(FilterSet): - f = CharFilter(method='filter_f') + f = CharFilter(method="filter_f") class Meta: model = User @@ -906,40 +894,40 @@ def filter_f(inner_self, qs, name, value): self.assertIs(inner_self.request, request) return qs - F({'f': 'foo'}, request=request, queryset=User.objects.all()).qs + F({"f": "foo"}, request=request, queryset=User.objects.all()).qs def test_method_unresolvable(self): class F(FilterSet): - f = Filter(method='filter_f') + f = Filter(method="filter_f") f = F({}, queryset=User.objects.all()) with self.assertRaises(AssertionError) as w: - f.filters['f'].filter(User.objects.all(), 0) + f.filters["f"].filter(User.objects.all(), 0) - self.assertIn('%s.%s' % (F.__module__, F.__name__), str(w.exception)) - self.assertIn('.filter_f()', str(w.exception)) + self.assertIn("%s.%s" % (F.__module__, F.__name__), str(w.exception)) + self.assertIn(".filter_f()", str(w.exception)) def test_method_uncallable(self): class F(FilterSet): - f = Filter(method='filter_f') + f = Filter(method="filter_f") filter_f = 4 f = F({}, queryset=User.objects.all()) with self.assertRaises(AssertionError) as w: - f.filters['f'].filter(User.objects.all(), 0) + f.filters["f"].filter(User.objects.all(), 0) - self.assertIn('%s.%s' % (F.__module__, F.__name__), str(w.exception)) - self.assertIn('.filter_f()', str(w.exception)) + self.assertIn("%s.%s" % (F.__module__, F.__name__), str(w.exception)) + self.assertIn(".filter_f()", str(w.exception)) def test_method_set_unset(self): # use a mock to bypass bound/unbound method equality class TestFilter(Filter): filter = mock.Mock() - f = TestFilter(method='filter_f') - self.assertEqual(f.method, 'filter_f') + f = TestFilter(method="filter_f") + self.assertEqual(f.method, "filter_f") self.assertIsInstance(f.filter, FilterMethod) # setting None should revert to Filter.filter @@ -949,16 +937,15 @@ class TestFilter(Filter): class MiscFilterSetTests(TestCase): - def test_no__getitem__(self): # The DTL processes variable lookups by the following rules: # https://docs.djangoproject.com/en/stable/ref/templates/language/#variables # A __getitem__ implementation precedes normal attribute access, and in # the case of #58, will force the queryset to evaluate when it should # not (eg, when rendering a blank form). - self.assertFalse(hasattr(FilterSet, '__getitem__')) + self.assertFalse(hasattr(FilterSet, "__getitem__")) def test_no_qs_proxying(self): # The FilterSet should not proxy .qs methods - just access .qs directly - self.assertFalse(hasattr(FilterSet, '__len__')) - self.assertFalse(hasattr(FilterSet, '__iter__')) + self.assertFalse(hasattr(FilterSet, "__len__")) + self.assertFalse(hasattr(FilterSet, "__iter__")) diff --git a/tests/test_forms.py b/tests/test_forms.py index 568d7482a..013c677b9 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -8,7 +8,6 @@ class FilterSetFormTests(TestCase): - def test_form_from_empty_filterset(self): class F(FilterSet): pass @@ -20,11 +19,11 @@ def test_form(self): class F(FilterSet): class Meta: model = Book - fields = ('title',) + fields = ("title",) f = F().form self.assertIsInstance(f, forms.Form) - self.assertEqual(list(f.fields), ['title']) + self.assertEqual(list(f.fields), ["title"]) def test_custom_form(self): class MyForm(forms.Form): @@ -33,7 +32,7 @@ class MyForm(forms.Form): class F(FilterSet): class Meta: model = Book - fields = '__all__' + fields = "__all__" form = MyForm f = F().form @@ -43,26 +42,25 @@ def test_form_prefix(self): class F(FilterSet): class Meta: model = Book - fields = ('title',) + fields = ("title",) f = F().form self.assertIsNone(f.prefix) - f = F(prefix='prefix').form - self.assertEqual(f.prefix, 'prefix') + f = F(prefix="prefix").form + self.assertEqual(f.prefix, "prefix") def test_form_fields(self): class F(FilterSet): class Meta: model = User - fields = ['status'] + fields = ["status"] f = F().form self.assertEqual(len(f.fields), 1) - self.assertIn('status', f.fields) + self.assertIn("status", f.fields) self.assertSequenceEqual( - list(f.fields['status'].choices), - (('', '---------'), ) + STATUS_CHOICES + list(f.fields["status"].choices), (("", "---------"),) + STATUS_CHOICES ) def test_form_fields_exclusion(self): @@ -71,53 +69,58 @@ class F(FilterSet): class Meta: model = Book - fields = ('title',) + fields = ("title",) f = F().form - self.assertEqual(f.fields['title'].label, "Exclude title") + self.assertEqual(f.fields["title"].label, "Exclude title") def test_complex_form_fields(self): class F(FilterSet): - username = CharFilter(label='Filter for users with username') - exclude_username = CharFilter(field_name='username', lookup_expr='iexact', exclude=True) + username = CharFilter(label="Filter for users with username") + exclude_username = CharFilter( + field_name="username", lookup_expr="iexact", exclude=True + ) class Meta: model = User fields = { - 'status': ['exact', 'lt', 'gt'], - 'favorite_books__title': ['iexact', 'in'], - 'manager_of__users__username': ['exact'], + "status": ["exact", "lt", "gt"], + "favorite_books__title": ["iexact", "in"], + "manager_of__users__username": ["exact"], } fields = F().form.fields - self.assertEqual(fields['username'].label, 'Filter for users with username') - self.assertEqual(fields['exclude_username'].label, 'Exclude username') - self.assertEqual(fields['status'].label, 'Status') - self.assertEqual(fields['status__lt'].label, 'Status is less than') - self.assertEqual(fields['status__gt'].label, 'Status is greater than') - self.assertEqual(fields['favorite_books__title__iexact'].label, 'Favorite books title') - self.assertEqual(fields['favorite_books__title__in'].label, 'Favorite books title is in') - self.assertEqual(fields['manager_of__users__username'].label, 'Manager of users username') + self.assertEqual(fields["username"].label, "Filter for users with username") + self.assertEqual(fields["exclude_username"].label, "Exclude username") + self.assertEqual(fields["status"].label, "Status") + self.assertEqual(fields["status__lt"].label, "Status is less than") + self.assertEqual(fields["status__gt"].label, "Status is greater than") + self.assertEqual( + fields["favorite_books__title__iexact"].label, "Favorite books title" + ) + self.assertEqual( + fields["favorite_books__title__in"].label, "Favorite books title is in" + ) + self.assertEqual( + fields["manager_of__users__username"].label, "Manager of users username" + ) def test_form_fields_using_widget(self): class F(FilterSet): - status = ChoiceFilter(widget=forms.RadioSelect, - choices=STATUS_CHOICES, - empty_label=None) + status = ChoiceFilter( + widget=forms.RadioSelect, choices=STATUS_CHOICES, empty_label=None + ) class Meta: model = User - fields = ['status', 'username'] + fields = ["status", "username"] f = F().form self.assertEqual(len(f.fields), 2) - self.assertIn('status', f.fields) - self.assertIn('username', f.fields) - self.assertSequenceEqual( - list(f.fields['status'].choices), - STATUS_CHOICES - ) - self.assertIsInstance(f.fields['status'].widget, forms.RadioSelect) + self.assertIn("status", f.fields) + self.assertIn("username", f.fields) + self.assertSequenceEqual(list(f.fields["status"].choices), STATUS_CHOICES) + self.assertIsInstance(f.fields["status"].widget, forms.RadioSelect) def test_form_field_with_custom_label(self): class F(FilterSet): @@ -125,35 +128,35 @@ class F(FilterSet): class Meta: model = Book - fields = ('title',) + fields = ("title",) f = F().form - self.assertEqual(f.fields['title'].label, "Book title") - self.assertEqual(f['title'].label, 'Book title') + self.assertEqual(f.fields["title"].label, "Book title") + self.assertEqual(f["title"].label, "Book title") def test_form_field_with_manual_name(self): class F(FilterSet): - book_title = CharFilter(field_name='title') + book_title = CharFilter(field_name="title") class Meta: model = Book - fields = ('book_title',) + fields = ("book_title",) f = F().form - self.assertEqual(f.fields['book_title'].label, "Title") - self.assertEqual(f['book_title'].label, "Title") + self.assertEqual(f.fields["book_title"].label, "Title") + self.assertEqual(f["book_title"].label, "Title") def test_form_field_with_manual_name_and_label(self): class F(FilterSet): - f1 = CharFilter(field_name='title', label="Book title") + f1 = CharFilter(field_name="title", label="Book title") class Meta: model = Book - fields = ('f1',) + fields = ("f1",) f = F().form - self.assertEqual(f.fields['f1'].label, "Book title") - self.assertEqual(f['f1'].label, 'Book title') + self.assertEqual(f.fields["f1"].label, "Book title") + self.assertEqual(f["f1"].label, "Book title") def test_filter_with_initial(self): class F(FilterSet): @@ -161,16 +164,16 @@ class F(FilterSet): class Meta: model = User - fields = ['status'] + fields = ["status"] f = F().form - self.assertEqual(f.fields['status'].initial, 1) + self.assertEqual(f.fields["status"].initial, 1) def test_form_is_not_bound(self): class F(FilterSet): class Meta: model = Book - fields = ('title',) + fields = ("title",) f = F().form self.assertFalse(f.is_bound) @@ -180,27 +183,26 @@ def test_form_is_bound(self): class F(FilterSet): class Meta: model = Book - fields = ('title',) + fields = ("title",) - f = F({'title': 'Some book'}).form + f = F({"title": "Some book"}).form self.assertTrue(f.is_bound) - self.assertEqual(f.data, {'title': 'Some book'}) + self.assertEqual(f.data, {"title": "Some book"}) def test_limit_choices_to(self): - User.objects.create(username='inactive', is_active=False, status=REGULAR) - User.objects.create(username='active', is_active=True, status=REGULAR) - User.objects.create(username='manager', is_active=False, status=MANAGER) + User.objects.create(username="inactive", is_active=False, status=REGULAR) + User.objects.create(username="active", is_active=True, status=REGULAR) + User.objects.create(username="manager", is_active=False, status=MANAGER) class F(FilterSet): class Meta: model = ManagerGroup - fields = ['users', 'manager'] + fields = ["users", "manager"] + f = F().form + self.assertEqual(list(f.fields["users"].choices), [(2, "active")]) self.assertEqual( - list(f.fields['users'].choices), [(2, 'active')] - ) - self.assertEqual( - list(f.fields['manager'].choices), [('', '---------'), (3, 'manager')] + list(f.fields["manager"].choices), [("", "---------"), (3, "manager")] ) def test_disabled_help_text(self): @@ -209,28 +211,24 @@ class Meta: model = Book fields = { # 'in' lookups are CSV-based, which have a `help_text`. - 'title': ['in'] + "title": ["in"] } self.assertEqual( - F().form.fields['title__in'].help_text, - 'Multiple values may be separated by commas.' + F().form.fields["title__in"].help_text, + "Multiple values may be separated by commas.", ) with override_settings(FILTERS_DISABLE_HELP_TEXT=True): - self.assertEqual( - F().form.fields['title__in'].help_text, - '' - ) + self.assertEqual(F().form.fields["title__in"].help_text, "") class FilterSetValidityTests(TestCase): - class F(FilterSet): class Meta: model = Book - fields = ['title', 'price'] + fields = ["title", "price"] def test_not_bound(self): f = self.F() @@ -241,31 +239,31 @@ def test_not_bound(self): self.assertEqual(f.errors, {}) def test_is_bound_and_valid(self): - f = self.F({'title': 'Some book'}) + f = self.F({"title": "Some book"}) self.assertTrue(f.is_bound) self.assertTrue(f.is_valid()) - self.assertEqual(f.data, {'title': 'Some book'}) + self.assertEqual(f.data, {"title": "Some book"}) self.assertEqual(f.errors, {}) def test_is_bound_and_not_valid(self): - f = self.F({'price': 'four dollars'}) + f = self.F({"price": "four dollars"}) self.assertTrue(f.is_bound) self.assertFalse(f.is_valid()) - self.assertEqual(f.data, {'price': 'four dollars'}) - self.assertEqual(f.errors, {'price': ['Enter a number.']}) + self.assertEqual(f.data, {"price": "four dollars"}) + self.assertEqual(f.errors, {"price": ["Enter a number."]}) def test_number_filter_max_value_validation(self): class F(FilterSet): class Meta: model = Book - fields = ['average_rating'] + fields = ["average_rating"] - f = F({'average_rating': '1E1001'}) + f = F({"average_rating": "1E1001"}) self.assertTrue(f.is_bound) self.assertFalse(f.is_valid()) self.assertEqual( f.errors, - {'average_rating': ['Ensure this value is less than or equal to 1e+50.']} + {"average_rating": ["Ensure this value is less than or equal to 1e+50."]}, ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 7a57fae46..305fc7270 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,12 +1,14 @@ import datetime +import unittest import warnings +import django from django.db import models from django.db.models.constants import LOOKUP_SEP from django.db.models.fields.related import ForeignObjectRel from django.test import TestCase, override_settings from django.utils.functional import Promise -from django.utils.timezone import get_default_timezone +from django.utils.timezone import get_default_timezone, make_aware from django_filters import FilterSet from django_filters.exceptions import FieldLookupError @@ -21,33 +23,22 @@ resolve_field, translate_validation, verbose_field_name, - verbose_lookup_expr + verbose_lookup_expr, ) -from .models import ( - Article, - Book, - Business, - Company, - HiredWorker, - NetworkSetting, - User -) +from .models import Article, Book, Business, Company, HiredWorker, NetworkSetting, User class MigrationNoticeTests(TestCase): - def test_message(self): self.assertEqual( - str(MigrationNotice('Message.')), - 'Message. See: https://django-filter.readthedocs.io/en/master/guide/migration.html' + str(MigrationNotice("Message.")), + "Message. See: https://django-filter.readthedocs.io/en/main/guide/migration.html", ) class RenameAttributes(RenameAttributesBase): - renamed_attributes = ( - ('old', 'new', DeprecationWarning), - ) + renamed_attributes = (("old", "new", DeprecationWarning),) class SENTINEL: @@ -55,9 +46,8 @@ class SENTINEL: class RenameAttributesBaseTests(TestCase): - def check(self, recorded, count): - expected = '`Example.old` attribute should be renamed `new`.' + expected = "`Example.old` attribute should be renamed `new`." self.assertEqual(len(recorded), count) for _ in range(count): @@ -67,7 +57,7 @@ def check(self, recorded, count): def test_class_creation_warnings(self): with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') + warnings.simplefilter("always") class Example(metaclass=RenameAttributes): old = SENTINEL @@ -77,16 +67,16 @@ class Example(metaclass=RenameAttributes): def test_renamed_attribute_in_class_dict(self): with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") class Example(metaclass=RenameAttributes): old = SENTINEL - warnings.simplefilter('always') + warnings.simplefilter("always") # Ensure `old` and `new` are not both in class dict. - self.assertNotIn('old', Example.__dict__) - self.assertIn('new', Example.__dict__) + self.assertNotIn("old", Example.__dict__) + self.assertIn("new", Example.__dict__) # Ensure `old` value assigned to `new`. self.assertEqual(Example.new, SENTINEL) @@ -95,12 +85,12 @@ class Example(metaclass=RenameAttributes): def test_class_accessor_warnings(self): with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") class Example(metaclass=RenameAttributes): new = None - warnings.simplefilter('always') + warnings.simplefilter("always") self.assertIsNone(Example.new) self.assertIsNone(Example.old) @@ -113,12 +103,12 @@ class Example(metaclass=RenameAttributes): def test_instance_accessor_warnings(self): with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") class Example(metaclass=RenameAttributes): new = None - warnings.simplefilter('always') + warnings.simplefilter("always") example = Example() self.check(recorded, 0) @@ -134,7 +124,7 @@ class Example(metaclass=RenameAttributes): def test_class_instance_values(self): with warnings.catch_warnings(record=True): - warnings.simplefilter('ignore') + warnings.simplefilter("ignore") class Example(metaclass=RenameAttributes): new = None @@ -150,11 +140,11 @@ class Example(metaclass=RenameAttributes): def test_getter_reachable(self): with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') + warnings.simplefilter("always") class Example(metaclass=RenameAttributes): def __getattr__(self, name): - if name == 'test': + if name == "test": return SENTINEL return self.__getattribute__(name) @@ -164,11 +154,11 @@ def __getattr__(self, name): def test_parent_getter_reachable(self): with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') + warnings.simplefilter("always") class Parent: def __getattr__(self, name): - if name == 'test': + if name == "test": return SENTINEL return self.__getattribute__(name) @@ -181,11 +171,11 @@ class Example(Parent, metaclass=RenameAttributes): def test_setter_reachable(self): with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') + warnings.simplefilter("always") class Example(metaclass=RenameAttributes): def __setattr__(self, name, value): - if name == 'test': + if name == "test": value = SENTINEL super().__setattr__(name, value) @@ -196,55 +186,71 @@ def __setattr__(self, name, value): class GetFieldPartsTests(TestCase): - def test_field(self): - parts = get_field_parts(User, 'username') + parts = get_field_parts(User, "username") self.assertEqual(len(parts), 1) self.assertIsInstance(parts[0], models.CharField) def test_non_existent_field(self): - result = get_model_field(User, 'unknown__name') + result = get_model_field(User, "unknown__name") self.assertIsNone(result) def test_forwards_related_field(self): - parts = get_field_parts(User, 'favorite_books__title') + parts = get_field_parts(User, "favorite_books__title") self.assertEqual(len(parts), 2) self.assertIsInstance(parts[0], models.ManyToManyField) self.assertIsInstance(parts[1], models.CharField) def test_reverse_related_field(self): - parts = get_field_parts(User, 'manager_of__users__username') + parts = get_field_parts(User, "manager_of__users__username") self.assertEqual(len(parts), 3) self.assertIsInstance(parts[0], ForeignObjectRel) self.assertIsInstance(parts[1], models.ManyToManyField) self.assertIsInstance(parts[2], models.CharField) + def test_lazy_relationship_not_ready(self): + """ + This simulates trying to create a FilterSet before the app registry has + been populated. Lazy relationships have not yet been resolved from their + strings into their remote model references. + """ -class GetModelFieldTests(TestCase): + class TestModel(models.Model): + fk = models.ForeignKey("remote.Model", on_delete=models.CASCADE) + msg = ( + "Unable to resolve relationship `fk__f` for `tests.TestModel`. " + "Django is most likely not initialized, and its apps registry " + "not populated. Ensure Django has finished setup before loading " + "`FilterSet`s." + ) + with self.assertRaisesMessage(RuntimeError, msg): + get_field_parts(TestModel, "fk__f") + + +class GetModelFieldTests(TestCase): def test_non_existent_field(self): - result = get_model_field(User, 'unknown__name') + result = get_model_field(User, "unknown__name") self.assertIsNone(result) def test_related_field(self): - result = get_model_field(Business, 'hiredworker__worker') - self.assertEqual(result, HiredWorker._meta.get_field('worker')) + result = get_model_field(Business, "hiredworker__worker") + self.assertEqual(result, HiredWorker._meta.get_field("worker")) class ResolveFieldTests(TestCase): - def test_resolve_plain_lookups(self): """ Check that the standard query terms can be correctly resolved. eg, an 'EXACT' lookup on a user's username """ - model_field = User._meta.get_field('username') + model_field = User._meta.get_field("username") lookups = model_field.class_lookups.keys() - # This is simple - the final ouput of an untransformed field is itself. + # This is simple - the final output of an untransformed field is itself. # The lookups are the default lookups registered to the class. for term in lookups: field, lookup = resolve_field(model_field, term) @@ -256,17 +262,25 @@ def test_resolve_forward_related_lookups(self): Check that lookups can be resolved for related fields in the forwards direction. """ - lookups = ['exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ] + lookups = [ + "exact", + "gte", + "gt", + "lte", + "lt", + "in", + "isnull", + ] # ForeignKey - model_field = Article._meta.get_field('author') + model_field = Article._meta.get_field("author") for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ForeignKey) self.assertEqual(lookup, term) # ManyToManyField - model_field = User._meta.get_field('favorite_books') + model_field = User._meta.get_field("favorite_books") for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToManyField) @@ -277,17 +291,25 @@ def test_resolve_reverse_related_lookups(self): Check that lookups can be resolved for related fields in the reverse direction. """ - lookups = ['exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ] + lookups = [ + "exact", + "gte", + "gt", + "lte", + "lt", + "in", + "isnull", + ] # ManyToOneRel - model_field = User._meta.get_field('article') + model_field = User._meta.get_field("article") for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToOneRel) self.assertEqual(lookup, term) # ManyToManyRel - model_field = Book._meta.get_field('lovers') + model_field = Book._meta.get_field("lovers") for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToManyRel) @@ -300,252 +322,278 @@ def test_resolve_transformed_lookups(self): """ # Use a DateTimeField, so we can check multiple transforms. # eg, date__year__gte - model_field = Article._meta.get_field('published') + model_field = Article._meta.get_field("published") standard_lookups = [ - 'exact', - 'iexact', - 'gte', - 'gt', - 'lte', - 'lt', + "exact", + "iexact", + "gte", + "gt", + "lte", + "lt", ] date_lookups = [ - 'year', - 'month', - 'day', - 'week_day', + "year", + "month", + "day", + "week_day", ] datetime_lookups = date_lookups + [ - 'hour', - 'minute', - 'second', + "hour", + "minute", + "second", ] # ex: 'date__gt' for lookup in standard_lookups: - field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join(['date', lookup])) + field, resolved_lookup = resolve_field( + model_field, LOOKUP_SEP.join(["date", lookup]) + ) self.assertIsInstance(field, models.DateField) self.assertEqual(resolved_lookup, lookup) # ex: 'year__iexact' for part in datetime_lookups: for lookup in standard_lookups: - field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join([part, lookup])) + field, resolved_lookup = resolve_field( + model_field, LOOKUP_SEP.join([part, lookup]) + ) self.assertIsInstance(field, models.IntegerField) self.assertEqual(resolved_lookup, lookup) # ex: 'date__year__lte' for part in date_lookups: for lookup in standard_lookups: - field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join(['date', part, lookup])) + field, resolved_lookup = resolve_field( + model_field, LOOKUP_SEP.join(["date", part, lookup]) + ) self.assertIsInstance(field, models.IntegerField) self.assertEqual(resolved_lookup, lookup) def test_resolve_implicit_exact_lookup(self): # Use a DateTimeField, so we can check multiple transforms. # eg, date__year__gte - model_field = Article._meta.get_field('published') + model_field = Article._meta.get_field("published") - field, lookup = resolve_field(model_field, 'date') + field, lookup = resolve_field(model_field, "date") self.assertIsInstance(field, models.DateField) - self.assertEqual(lookup, 'exact') + self.assertEqual(lookup, "exact") - field, lookup = resolve_field(model_field, 'date__year') + field, lookup = resolve_field(model_field, "date__year") self.assertIsInstance(field, models.IntegerField) - self.assertEqual(lookup, 'exact') + self.assertEqual(lookup, "exact") def test_invalid_lookup_expression(self): - model_field = Article._meta.get_field('published') + model_field = Article._meta.get_field("published") with self.assertRaises(FieldLookupError) as context: - resolve_field(model_field, 'invalid_lookup') + resolve_field(model_field, "invalid_lookup") exc = str(context.exception) self.assertIn(str(model_field), exc) - self.assertIn('invalid_lookup', exc) + self.assertIn("invalid_lookup", exc) def test_invalid_transformed_lookup_expression(self): - model_field = Article._meta.get_field('published') + model_field = Article._meta.get_field("published") with self.assertRaises(FieldLookupError) as context: - resolve_field(model_field, 'date__invalid_lookup') + resolve_field(model_field, "date__invalid_lookup") exc = str(context.exception) self.assertIn(str(model_field), exc) - self.assertIn('date__invalid_lookup', exc) + self.assertIn("date__invalid_lookup", exc) class VerboseFieldNameTests(TestCase): - def test_none(self): verbose_name = verbose_field_name(Article, None) - self.assertEqual(verbose_name, '[invalid name]') + self.assertEqual(verbose_name, "[invalid name]") def test_invalid_name(self): - verbose_name = verbose_field_name(Article, 'foobar') - self.assertEqual(verbose_name, '[invalid name]') + verbose_name = verbose_field_name(Article, "foobar") + self.assertEqual(verbose_name, "[invalid name]") def test_field(self): - verbose_name = verbose_field_name(Article, 'author') - self.assertEqual(verbose_name, 'author') + verbose_name = verbose_field_name(Article, "author") + self.assertEqual(verbose_name, "author") def test_field_with_verbose_name(self): - verbose_name = verbose_field_name(Article, 'name') - self.assertEqual(verbose_name, 'title') + verbose_name = verbose_field_name(Article, "name") + self.assertEqual(verbose_name, "title") def test_field_all_caps(self): - verbose_name = verbose_field_name(NetworkSetting, 'cidr') - self.assertEqual(verbose_name, 'CIDR') + verbose_name = verbose_field_name(NetworkSetting, "cidr") + self.assertEqual(verbose_name, "CIDR") def test_forwards_related_field(self): - verbose_name = verbose_field_name(Article, 'author__username') - self.assertEqual(verbose_name, 'author username') + verbose_name = verbose_field_name(Article, "author__username") + self.assertEqual(verbose_name, "author username") def test_backwards_related_field(self): - verbose_name = verbose_field_name(Book, 'lovers__first_name') - self.assertEqual(verbose_name, 'lovers first name') + verbose_name = verbose_field_name(Book, "lovers__first_name") + self.assertEqual(verbose_name, "lovers first name") def test_backwards_related_field_multi_word(self): - verbose_name = verbose_field_name(User, 'manager_of') - self.assertEqual(verbose_name, 'manager of') + verbose_name = verbose_field_name(User, "manager_of") + self.assertEqual(verbose_name, "manager of") def test_lazy_text(self): # sanity check - field = User._meta.get_field('username') + field = User._meta.get_field("username") self.assertIsInstance(field.verbose_name, Promise) - verbose_name = verbose_field_name(User, 'username') - self.assertEqual(verbose_name, 'username') + verbose_name = verbose_field_name(User, "username") + self.assertEqual(verbose_name, "username") def test_forwards_fk(self): - verbose_name = verbose_field_name(Article, 'author') - self.assertEqual(verbose_name, 'author') + verbose_name = verbose_field_name(Article, "author") + self.assertEqual(verbose_name, "author") def test_backwards_fk(self): # https://github.com/carltongibson/django-filter/issues/716 # related_name is set - verbose_name = verbose_field_name(Company, 'locations') - self.assertEqual(verbose_name, 'locations') + verbose_name = verbose_field_name(Company, "locations") + self.assertEqual(verbose_name, "locations") # related_name not set. Auto-generated relation is `article_set` # _meta.get_field raises FieldDoesNotExist - verbose_name = verbose_field_name(User, 'article_set') - self.assertEqual(verbose_name, '[invalid name]') + verbose_name = verbose_field_name(User, "article_set") + self.assertEqual(verbose_name, "[invalid name]") # WRONG NAME! Returns ManyToOneRel with related_name == None. - verbose_name = verbose_field_name(User, 'article') - self.assertEqual(verbose_name, '[invalid name]') + verbose_name = verbose_field_name(User, "article") + self.assertEqual(verbose_name, "[invalid name]") class VerboseLookupExprTests(TestCase): - def test_exact(self): # Exact should default to empty. A verbose expression is unnecessary, # and this behavior works well with list syntax for `Meta.fields`. - verbose_lookup = verbose_lookup_expr('exact') - self.assertEqual(verbose_lookup, '') + verbose_lookup = verbose_lookup_expr("exact") + self.assertEqual(verbose_lookup, "") def test_verbose_expression(self): - verbose_lookup = verbose_lookup_expr('date__lt') - self.assertEqual(verbose_lookup, 'date is less than') + verbose_lookup = verbose_lookup_expr("date__lt") + self.assertEqual(verbose_lookup, "date is less than") def test_missing_keys(self): - verbose_lookup = verbose_lookup_expr('foo__bar__lt') - self.assertEqual(verbose_lookup, 'foo bar is less than') + verbose_lookup = verbose_lookup_expr("foo__bar__lt") + self.assertEqual(verbose_lookup, "foo bar is less than") - @override_settings(FILTERS_VERBOSE_LOOKUPS={'exact': 'is equal to'}) + @override_settings(FILTERS_VERBOSE_LOOKUPS={"exact": "is equal to"}) def test_overridden_settings(self): - verbose_lookup = verbose_lookup_expr('exact') - self.assertEqual(verbose_lookup, 'is equal to') + verbose_lookup = verbose_lookup_expr("exact") + self.assertEqual(verbose_lookup, "is equal to") class LabelForFilterTests(TestCase): - def test_standard_label(self): - label = label_for_filter(Article, 'name', 'in') - self.assertEqual(label, 'Title is in') + label = label_for_filter(Article, "name", "in") + self.assertEqual(label, "Title is in") def test_related_model(self): - label = label_for_filter(Article, 'author__first_name', 'in') - self.assertEqual(label, 'Author first name is in') + label = label_for_filter(Article, "author__first_name", "in") + self.assertEqual(label, "Author first name is in") def test_exclusion_label(self): - label = label_for_filter(Article, 'name', 'in', exclude=True) - self.assertEqual(label, 'Exclude title is in') + label = label_for_filter(Article, "name", "in", exclude=True) + self.assertEqual(label, "Exclude title is in") def test_related_model_exclusion(self): - label = label_for_filter(Article, 'author__first_name', 'in', exclude=True) - self.assertEqual(label, 'Exclude author first name is in') + label = label_for_filter(Article, "author__first_name", "in", exclude=True) + self.assertEqual(label, "Exclude author first name is in") def test_exact_lookup(self): - label = label_for_filter(Article, 'name', 'exact') - self.assertEqual(label, 'Title') + label = label_for_filter(Article, "name", "exact") + self.assertEqual(label, "Title") def test_field_all_caps(self): - label = label_for_filter(NetworkSetting, 'cidr', 'contains', exclude=True) - self.assertEqual(label, 'Exclude CIDR contains') + label = label_for_filter(NetworkSetting, "cidr", "contains", exclude=True) + self.assertEqual(label, "Exclude CIDR contains") +@unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") class HandleTimezone(TestCase): - - @override_settings(TIME_ZONE='America/Sao_Paulo') + @override_settings(TIME_ZONE="America/Sao_Paulo") def test_handle_dst_ending(self): dst_ending_date = datetime.datetime(2017, 2, 18, 23, 59, 59, 999999) handled = handle_timezone(dst_ending_date, False) - self.assertEqual(handled, get_default_timezone().localize(dst_ending_date, False)) + self.assertEqual( + handled, + make_aware(dst_ending_date, get_default_timezone(), False), + ) - @override_settings(TIME_ZONE='America/Sao_Paulo') + @override_settings(TIME_ZONE="America/Sao_Paulo") def test_handle_dst_starting(self): dst_starting_date = datetime.datetime(2017, 10, 15, 0, 0, 0, 0) handled = handle_timezone(dst_starting_date, True) - self.assertEqual(handled, get_default_timezone().localize(dst_starting_date, True)) + self.assertEqual( + handled, + make_aware(dst_starting_date, get_default_timezone(), True), + ) class TranslateValidationDataTests(TestCase): - class F(FilterSet): class Meta: model = Article - fields = ['id', 'author', 'name'] + fields = ["id", "author", "name"] - choice = MultipleChoiceFilter(choices=[('1', 'one'), ('2', 'two')]) + choice = MultipleChoiceFilter(choices=[("1", "one"), ("2", "two")]) def test_error_detail(self): - f = self.F(data={ - 'id': 'foo', - 'author': 'bar', - 'name': 'baz', - 'choice': ['3'], - }) + f = self.F( + data={ + "id": "foo", + "author": "bar", + "name": "baz", + "choice": ["3"], + } + ) exc = translate_validation(f.errors) - self.assertDictEqual(exc.detail, { - 'id': ['Enter a number.'], - 'author': ['Select a valid choice. That choice is not one of the available choices.'], - 'choice': ['Select a valid choice. 3 is not one of the available choices.'], - }) + self.assertDictEqual( + exc.detail, + { + "id": ["Enter a number."], + "author": [ + "Select a valid choice. That choice is not one of the available choices." + ], + "choice": [ + "Select a valid choice. 3 is not one of the available choices." + ], + }, + ) def test_full_error_details(self): - f = self.F(data={ - 'id': 'foo', - 'author': 'bar', - 'name': 'baz', - 'choice': ['3'], - }) + f = self.F( + data={ + "id": "foo", + "author": "bar", + "name": "baz", + "choice": ["3"], + } + ) exc = translate_validation(f.errors) - self.assertEqual(exc.get_full_details(), { - 'id': [{'message': 'Enter a number.', 'code': 'invalid'}], - 'author': [{ - 'message': 'Select a valid choice. That choice is not one of the available choices.', - 'code': 'invalid_choice', - }], - 'choice': [{ - 'message': 'Select a valid choice. 3 is not one of the available choices.', - 'code': 'invalid_choice', - }], - }) + self.assertEqual( + exc.get_full_details(), + { + "id": [{"message": "Enter a number.", "code": "invalid"}], + "author": [ + { + "message": "Select a valid choice. That choice is not one of the available choices.", + "code": "invalid_choice", + } + ], + "choice": [ + { + "message": "Select a valid choice. 3 is not one of the available choices.", + "code": "invalid_choice", + } + ], + }, + ) diff --git a/tests/test_views.py b/tests/test_views.py index c6bd776e3..98d84797a 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,5 +1,3 @@ -import warnings - from django.core.exceptions import ImproperlyConfigured from django.test import TestCase, override_settings from django.test.client import RequestFactory @@ -11,31 +9,27 @@ from .models import Book -@override_settings(ROOT_URLCONF='tests.urls') +@override_settings(ROOT_URLCONF="tests.urls") class GenericViewTestCase(TestCase): - def setUp(self): - Book.objects.create( - title="Ender's Game", price='1.00', average_rating=3.0) - Book.objects.create( - title="Rainbow Six", price='1.00', average_rating=3.0) - Book.objects.create( - title="Snowcrash", price='1.00', average_rating=3.0) + Book.objects.create(title="Ender's Game", price="1.00", average_rating=3.0) + Book.objects.create(title="Rainbow Six", price="1.00", average_rating=3.0) + Book.objects.create(title="Snowcrash", price="1.00", average_rating=3.0) class GenericClassBasedViewTests(GenericViewTestCase): - base_url = '/books/' + base_url = "/books/" def test_view(self): response = self.client.get(self.base_url) - for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) def test_view_filtering_on_title(self): - response = self.client.get(self.base_url + '?title=Snowcrash') - for b in ["Ender's Game", 'Rainbow Six']: + response = self.client.get(self.base_url + "?title=Snowcrash") + for b in ["Ender's Game", "Rainbow Six"]: self.assertNotContains(response, html.escape(b)) - self.assertContains(response, 'Snowcrash') + self.assertContains(response, "Snowcrash") def test_view_with_filterset_not_model(self): factory = RequestFactory() @@ -44,7 +38,7 @@ def test_view_with_filterset_not_model(self): view = FilterView.as_view(filterset_class=filterset) response = view(request) self.assertEqual(response.status_code, 200) - for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) def test_view_with_model_no_filterset(self): @@ -53,46 +47,49 @@ def test_view_with_model_no_filterset(self): view = FilterView.as_view(model=Book) response = view(request) self.assertEqual(response.status_code, 200) - for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) def test_view_with_model_and_fields_no_filterset(self): factory = RequestFactory() - request = factory.get(self.base_url + '?price=1.0') - view = FilterView.as_view(model=Book, filterset_fields=['price']) + request = factory.get(self.base_url + "?price=1.0") + view = FilterView.as_view(model=Book, filterset_fields=["price"]) # filtering only by price response = view(request) self.assertEqual(response.status_code, 200) - for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) # not filtering by title - request = factory.get(self.base_url + '?title=Snowcrash') + request = factory.get(self.base_url + "?title=Snowcrash") response = view(request) self.assertEqual(response.status_code, 200) - for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) def test_view_with_strict_errors(self): factory = RequestFactory() - request = factory.get(self.base_url + '?title=Snowcrash&price=four dollars') + request = factory.get(self.base_url + "?title=Snowcrash&price=four dollars") view = FilterView.as_view(model=Book) response = view(request) - titles = [o.title for o in response.context_data['object_list']] + titles = [o.title for o in response.context_data["object_list"]] self.assertEqual(response.status_code, 200) self.assertEqual(titles, []) def test_view_with_non_strict_errors(self): factory = RequestFactory() - request = factory.get(self.base_url + '?title=Snowcrash&price=four dollars') + request = factory.get(self.base_url + "?title=Snowcrash&price=four dollars") view = FilterView.as_view(model=Book, strict=False) response = view(request) - titles = [o.title for o in response.context_data['object_list']] + titles = [o.title for o in response.context_data["object_list"]] self.assertEqual(response.status_code, 200) - self.assertEqual(titles, ['Snowcrash'],) + self.assertEqual( + titles, + ["Snowcrash"], + ) def test_view_without_filterset_or_model(self): factory = RequestFactory() @@ -111,46 +108,33 @@ class MyFilterSet(FilterSet): with self.assertRaises(ImproperlyConfigured): view(request) - def test_filter_fields_removed(self): - expected = "`View.filter_fields` attribute should be renamed `filterset_fields`. " \ - "See: https://django-filter.readthedocs.io/en/master/guide/migration.html" - with warnings.catch_warnings(record=True) as recorded: - warnings.simplefilter('always') - - class View(FilterView): - filter_fields = None - - message = str(recorded.pop().message) - self.assertEqual(message, expected) - self.assertEqual(len(recorded), 0) - def test_view_with_unbound_filter_form_returns_initial_queryset(self): factory = RequestFactory() request = factory.get(self.base_url) - queryset = Book.objects.filter(title='Snowcrash') + queryset = Book.objects.filter(title="Snowcrash") view = FilterView.as_view(model=Book, queryset=queryset) response = view(request) - titles = [o.title for o in response.context_data['object_list']] + titles = [o.title for o in response.context_data["object_list"]] self.assertEqual(response.status_code, 200) - self.assertEqual(titles, ['Snowcrash']) + self.assertEqual(titles, ["Snowcrash"]) class GenericFunctionalViewTests(GenericViewTestCase): - base_url = '/books-legacy/' + base_url = "/books-legacy/" def test_view(self): response = self.client.get(self.base_url) - for b in ["Ender's Game", 'Rainbow Six', 'Snowcrash']: + for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) # extra context - self.assertEqual(response.context_data['foo'], 'bar') - self.assertEqual(response.context_data['bar'], 'foo') + self.assertEqual(response.context_data["foo"], "bar") + self.assertEqual(response.context_data["bar"], "foo") def test_view_filtering_on_price(self): - response = self.client.get(self.base_url + '?title=Snowcrash') - for b in ["Ender's Game", 'Rainbow Six']: + response = self.client.get(self.base_url + "?title=Snowcrash") + for b in ["Ender's Game", "Rainbow Six"]: self.assertNotContains(response, html.escape(b)) - self.assertContains(response, 'Snowcrash') + self.assertContains(response, "Snowcrash") diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 930da8ee2..f50cc1bed 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -9,117 +9,145 @@ LookupChoiceWidget, QueryArrayWidget, RangeWidget, - SuffixedMultiWidget + SuffixedMultiWidget, ) class LookupTypeWidgetTests(TestCase): - def test_widget_requires_field(self): with self.assertRaises(TypeError): LookupChoiceWidget() def test_widget_render(self): - widgets = [TextInput(), Select(choices=(('a', 'a'), ('b', 'b')))] + widgets = [TextInput(), Select(choices=(("a", "a"), ("b", "b")))] w = LookupChoiceWidget(widgets) - self.assertHTMLEqual(w.render('price', ''), """ + self.assertHTMLEqual( + w.render("price", ""), + """ """) + """, + ) - self.assertHTMLEqual(w.render('price', None), """ + self.assertHTMLEqual( + w.render("price", None), + """ """) + """, + ) - self.assertHTMLEqual(w.render('price', ['2', 'a']), """ + self.assertHTMLEqual( + w.render("price", ["2", "a"]), + """ """) + """, + ) class LinkWidgetTests(TestCase): - def test_widget_without_choices(self): w = LinkWidget() self.assertEqual(len(w.choices), 0) - self.assertHTMLEqual(w.render('price', ''), """
    """) + self.assertHTMLEqual(w.render("price", ""), """
      """) def test_widget(self): choices = ( - ('test-val1', 'test-label1'), - ('test-val2', 'test-label2'), + ("test-val1", "test-label1"), + ("test-val2", "test-label2"), ) w = LinkWidget(choices=choices) self.assertEqual(len(w.choices), 2) - self.assertHTMLEqual(w.render('price', ''), """ + self.assertHTMLEqual( + w.render("price", ""), + """ """) +
    """, + ) - self.assertHTMLEqual(w.render('price', None), """ + self.assertHTMLEqual( + w.render("price", None), + """ """) +
""", + ) - self.assertHTMLEqual(w.render('price', 'test-val1'), """ + self.assertHTMLEqual( + w.render("price", "test-val1"), + """ """) + """, + ) def test_widget_with_option_groups(self): choices = ( - ('Audio', ( - ('vinyl', 'Vinyl'), - ('cd', 'CD'), - )), - ('Video', ( - ('vhs', 'VHS Tape'), - ('dvd', 'DVD'), - )), - ('unknown', 'Unknown'), + ( + "Audio", + ( + ("vinyl", "Vinyl"), + ("cd", "CD"), + ), + ), + ( + "Video", + ( + ("vhs", "VHS Tape"), + ("dvd", "DVD"), + ), + ), + ("unknown", "Unknown"), ) w = LinkWidget(choices=choices) - self.assertHTMLEqual(w.render('media', ''), """ + self.assertHTMLEqual( + w.render("media", ""), + """ """) + """, + ) def test_widget_with_blank_choice(self): choices = ( - ('', '---------'), - ('test-val1', 'test-label1'), - ('test-val2', 'test-label2'), + ("", "---------"), + ("test-val1", "test-label1"), + ("test-val2", "test-label2"), ) w = LinkWidget(choices=choices) - self.assertHTMLEqual(w.render('price', ''), """ + self.assertHTMLEqual( + w.render("price", ""), + """ """) + """, + ) def test_widget_value_from_datadict(self): w = LinkWidget() - data = {'price': 'test-val1'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, 'test-val1') + data = {"price": "test-val1"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, "test-val1") class SuffixedMultiWidgetTests(TestCase): @@ -130,74 +158,89 @@ def test_assertions(self): # suffixes must be unique class W(SuffixedMultiWidget): - suffixes = ['a', 'a'] + suffixes = ["a", "a"] with self.assertRaises(AssertionError): W(widgets=[BooleanWidget, BooleanWidget]) # should succeed class W(SuffixedMultiWidget): - suffixes = ['a', 'b'] + suffixes = ["a", "b"] + W(widgets=[BooleanWidget, BooleanWidget]) def test_render(self): class W(SuffixedMultiWidget): - suffixes = ['min', 'max'] + suffixes = ["min", "max"] w = W(widgets=[TextInput, TextInput]) - self.assertHTMLEqual(w.render('price', ''), """ + self.assertHTMLEqual( + w.render("price", ""), + """ - """) + """, + ) # blank suffix class W(SuffixedMultiWidget): - suffixes = [None, 'lookup'] + suffixes = [None, "lookup"] w = W(widgets=[TextInput, TextInput]) - self.assertHTMLEqual(w.render('price', ''), """ + self.assertHTMLEqual( + w.render("price", ""), + """ - """) + """, + ) def test_value_from_datadict(self): class W(SuffixedMultiWidget): - suffixes = ['min', 'max'] + suffixes = ["min", "max"] w = W(widgets=[TextInput, TextInput]) - result = w.value_from_datadict({ - 'price_min': '1', - 'price_max': '2', - }, {}, 'price') - self.assertEqual(result, ['1', '2']) + result = w.value_from_datadict( + { + "price_min": "1", + "price_max": "2", + }, + {}, + "price", + ) + self.assertEqual(result, ["1", "2"]) - result = w.value_from_datadict({}, {}, 'price') + result = w.value_from_datadict({}, {}, "price") self.assertEqual(result, [None, None]) # blank suffix class W(SuffixedMultiWidget): - suffixes = ['', 'lookup'] + suffixes = ["", "lookup"] w = W(widgets=[TextInput, TextInput]) - result = w.value_from_datadict({ - 'price': '1', - 'price_lookup': 'lt', - }, {}, 'price') - self.assertEqual(result, ['1', 'lt']) + result = w.value_from_datadict( + { + "price": "1", + "price_lookup": "lt", + }, + {}, + "price", + ) + self.assertEqual(result, ["1", "lt"]) def test_value_omitted_from_data(self): class A(SuffixedMultiWidget): - suffixes = ['b'] + suffixes = ["b"] a = A(widgets=[BooleanWidget]) - result = a.value_omitted_from_data([], None, 'test') + result = a.value_omitted_from_data([], None, "test") self.assertIsNotNone(result) def test_replace_name(self): class A(SuffixedMultiWidget): - suffixes = ['test'] + suffixes = ["test"] a = A(widgets=[None]) @@ -208,61 +251,70 @@ class A(SuffixedMultiWidget): def test_decompress_value_none(self): class A(SuffixedMultiWidget): - suffixes = [''] + suffixes = [""] a = A(widgets=[None]) self.assertEqual(a.decompress(None), [None, None]) class RangeWidgetTests(TestCase): - def test_widget(self): w = RangeWidget() self.assertEqual(len(w.widgets), 2) - self.assertHTMLEqual(w.render('price', ''), """ + self.assertHTMLEqual( + w.render("price", ""), + """ - - """) + """, + ) - self.assertHTMLEqual(w.render('price', slice(5.99, 9.99)), """ + self.assertHTMLEqual( + w.render("price", slice(5.99, 9.99)), + """ - - """) + """, + ) def test_widget_attributes(self): - w = RangeWidget(attrs={'type': 'date'}) + w = RangeWidget(attrs={"type": "date"}) self.assertEqual(len(w.widgets), 2) - self.assertHTMLEqual(w.render('date', ''), """ + self.assertHTMLEqual( + w.render("date", ""), + """ - - """) + """, + ) class BooleanWidgetTests(TestCase): - def test_widget_render(self): w = BooleanWidget() - self.assertHTMLEqual(w.render('price', ''), """ + self.assertHTMLEqual( + w.render("price", ""), + """ """) + """, + ) def test_widget_value_from_datadict(self): - """ - """ + """ """ w = BooleanWidget() - trueActive = {'active': 'true'} - result = w.value_from_datadict(trueActive, {}, 'active') + trueActive = {"active": "true"} + result = w.value_from_datadict(trueActive, {}, "active") self.assertEqual(result, True) - falseActive = {'active': 'false'} - result = w.value_from_datadict(falseActive, {}, 'active') + falseActive = {"active": "false"} + result = w.value_from_datadict(falseActive, {}, "active") self.assertEqual(result, False) - result = w.value_from_datadict({}, {}, 'active') + result = w.value_from_datadict({}, {}, "active") self.assertEqual(result, None) @@ -271,66 +323,87 @@ def test_widget_render(self): class NumberCSVWidget(BaseCSVWidget, NumberInput): pass - w = NumberCSVWidget(attrs={'test': 'attr'}) - self.assertHTMLEqual(w.render('price', None), """ - """) + w = NumberCSVWidget(attrs={"test": "attr"}) + self.assertHTMLEqual( + w.render("price", None), + """ + """, + ) - self.assertHTMLEqual(w.render('price', ''), """ - """) + self.assertHTMLEqual( + w.render("price", ""), + """ + """, + ) - self.assertHTMLEqual(w.render('price', []), """ - """) + self.assertHTMLEqual( + w.render("price", []), + """ + """, + ) - self.assertHTMLEqual(w.render('price', '1'), """ - """) + self.assertHTMLEqual( + w.render("price", "1"), + """ + """, + ) - self.assertHTMLEqual(w.render('price', '1,2'), """ - """) + self.assertHTMLEqual( + w.render("price", "1,2"), + """ + """, + ) - self.assertHTMLEqual(w.render('price', ['1', '2']), """ - """) + self.assertHTMLEqual( + w.render("price", ["1", "2"]), + """ + """, + ) - self.assertHTMLEqual(w.render('price', [1, 2]), """ - """) + self.assertHTMLEqual( + w.render("price", [1, 2]), + """ + """, + ) def test_widget_value_from_datadict(self): class NumberCSVWidget(BaseCSVWidget, NumberInput): pass w = NumberCSVWidget() - data = {'price': None} - result = w.value_from_datadict(data, {}, 'price') + data = {"price": None} + result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, None) - data = {'price': '1'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1']) + data = {"price": "1"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1"]) - data = {'price': '1,2'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1', '2']) + data = {"price": "1,2"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1", "2"]) - data = {'price': ['1', '2']} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1', '2']) + data = {"price": ["1", "2"]} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1", "2"]) - data = {'price': '1,,2'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1', '', '2']) + data = {"price": "1,,2"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1", "", "2"]) - data = {'price': '1,'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1', '']) + data = {"price": "1,"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1", ""]) - data = {'price': ','} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['', '']) + data = {"price": ","} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["", ""]) - data = {'price': ''} - result = w.value_from_datadict(data, {}, 'price') + data = {"price": ""} + result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) - result = w.value_from_datadict({}, {}, 'price') + result = w.value_from_datadict({}, {}, "price") self.assertEqual(result, None) def test_surrogate_class(self): @@ -351,27 +424,48 @@ class InstanceSurrogate(BaseCSVWidget, NumberInput): class CSVWidgetTests(TestCase): def test_widget_render(self): - w = CSVWidget(attrs={'test': 'attr'}) - self.assertHTMLEqual(w.render('price', None), """ - """) + w = CSVWidget(attrs={"test": "attr"}) + self.assertHTMLEqual( + w.render("price", None), + """ + """, + ) - self.assertHTMLEqual(w.render('price', ''), """ - """) + self.assertHTMLEqual( + w.render("price", ""), + """ + """, + ) - self.assertHTMLEqual(w.render('price', []), """ - """) + self.assertHTMLEqual( + w.render("price", []), + """ + """, + ) - self.assertHTMLEqual(w.render('price', '1'), """ - """) + self.assertHTMLEqual( + w.render("price", "1"), + """ + """, + ) - self.assertHTMLEqual(w.render('price', '1,2'), """ - """) + self.assertHTMLEqual( + w.render("price", "1,2"), + """ + """, + ) - self.assertHTMLEqual(w.render('price', ['1', '2']), """ - """) + self.assertHTMLEqual( + w.render("price", ["1", "2"]), + """ + """, + ) - self.assertHTMLEqual(w.render('price', [1, 2]), """ - """) + self.assertHTMLEqual( + w.render("price", [1, 2]), + """ + """, + ) class CSVSelectTests(TestCase): @@ -379,118 +473,125 @@ class CSVSelect(BaseCSVWidget, Select): pass def test_widget(self): - w = self.CSVSelect(choices=((1, 'a'), (2, 'b'))) + w = self.CSVSelect(choices=((1, "a"), (2, "b"))) self.assertHTMLEqual( - w.render('price', None), + w.render("price", None), """ - """ + """, ) self.assertHTMLEqual( - w.render('price', ''), + w.render("price", ""), """ - """) + """, + ) self.assertHTMLEqual( - w.render('price', '1'), + w.render("price", "1"), """ - """) + """, + ) self.assertHTMLEqual( - w.render('price', '1,2'), + w.render("price", "1,2"), """ - """ + """, ) - self.assertHTMLEqual(w.render('price', ['1', '2']), """ - """) + self.assertHTMLEqual( + w.render("price", ["1", "2"]), + """ + """, + ) - self.assertHTMLEqual(w.render('price', [1, 2]), """ - """) + self.assertHTMLEqual( + w.render("price", [1, 2]), + """ + """, + ) class QueryArrayWidgetTests(TestCase): - def test_widget_value_from_datadict(self): w = QueryArrayWidget() # Values can be provided as csv string: ?foo=bar,baz - data = {'price': None} - result = w.value_from_datadict(data, {}, 'price') + data = {"price": None} + result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) - data = {'price': '1'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1']) + data = {"price": "1"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1"]) - data = {'price': '1,2'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(sorted(result), ['1', '2']) + data = {"price": "1,2"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(sorted(result), ["1", "2"]) - data = {'price': '1,,2'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(sorted(result), ['1', '2']) + data = {"price": "1,,2"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(sorted(result), ["1", "2"]) - data = {'price': '1,'} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1']) + data = {"price": "1,"} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1"]) - data = {'price': ','} - result = w.value_from_datadict(data, {}, 'price') + data = {"price": ","} + result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) - data = {'price': ''} - result = w.value_from_datadict(data, {}, 'price') + data = {"price": ""} + result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) - result = w.value_from_datadict({}, {}, 'price') + result = w.value_from_datadict({}, {}, "price") self.assertEqual(result, []) # Values can be provided as query array: ?foo[]=bar&foo[]=baz - data = {'price[]': None} - result = w.value_from_datadict(data, {}, 'price') + data = {"price[]": None} + result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) - data = {'price[]': ['1']} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1']) + data = {"price[]": ["1"]} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1"]) - data = {'price[]': ['1', '2']} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(sorted(result), ['1', '2']) + data = {"price[]": ["1", "2"]} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(sorted(result), ["1", "2"]) - data = {'price[]': ['1', '', '2']} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(sorted(result), ['1', '2']) + data = {"price[]": ["1", "", "2"]} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(sorted(result), ["1", "2"]) - data = {'price[]': ['1', '']} - result = w.value_from_datadict(data, {}, 'price') - self.assertEqual(result, ['1']) + data = {"price[]": ["1", ""]} + result = w.value_from_datadict(data, {}, "price") + self.assertEqual(result, ["1"]) - data = {'price[]': ['', '']} - result = w.value_from_datadict(data, {}, 'price') + data = {"price[]": ["", ""]} + result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) - data = {'price[]': []} - result = w.value_from_datadict(data, {}, 'price') + data = {"price[]": []} + result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) - result = w.value_from_datadict({}, {}, 'price') + result = w.value_from_datadict({}, {}, "price") self.assertEqual(result, []) diff --git a/tests/urls.py b/tests/urls.py index dc2721070..c249b021b 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -6,10 +6,14 @@ def _foo(): - return 'bar' + return "bar" urlpatterns = [ - path('books-legacy/', object_filter, {'model': Book, 'extra_context': {'foo': _foo, 'bar': 'foo'}}), - path('books/', FilterView.as_view(model=Book)), + path( + "books-legacy/", + object_filter, + {"model": Book, "extra_context": {"foo": _foo, "bar": "foo"}}, + ), + path("books/", FilterView.as_view(model=Book)), ] diff --git a/tests/utils.py b/tests/utils.py index 3a4bd2585..627341c56 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,6 @@ class QuerySet(models.QuerySet): - def __bool__(self): return True diff --git a/tox.ini b/tox.ini index 7b1bbad08..21e590f99 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,16 @@ [tox] envlist = - {py35,py36,py37}-django22, - {py36,py37,py38}-django30, - {py36,py37,py38,py39}-django31, - {py38,py39}-latest, + {py37,py38,py39,py310}-django32, + {py38,py39}-{django40,django41,django42}, + {py310, py311}-{django41,django42,latest}, + {py310, py311, py312}-{django41,django42,django50,latest}, isort,lint,docs,warnings, +isolated_build = true [latest] deps = - https://github.com/django/django/archive/master.tar.gz + https://github.com/django/django/archive/main.tar.gz https://github.com/encode/django-rest-framework/archive/master.tar.gz [testenv] @@ -17,12 +18,14 @@ commands = coverage run --parallel-mode --source django_filters ./runtests.py -- setenv = PYTHONDONTWRITEBYTECODE=1 deps = - django22: django~=2.2.0 - django30: django~=3.0.0 - django31: django~=3.1.0 - !latest: djangorestframework~=3.12.0 + django32: django~=3.2.0 + django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 + django42: Django>=4.2rc1,<5.0 + django50: Django>=5.0b1,<5.1 + !latest: djangorestframework latest: {[latest]deps} - -rrequirements/test-ci.txt + -r requirements/test-ci.txt [testenv:isort] commands = isort --check-only --diff django_filters tests {posargs} @@ -35,8 +38,7 @@ deps = flake8 [testenv:docs] commands = sphinx-build -WE docs _docs deps = - sphinx - sphinx-rtd-theme + -rrequirements/docs.txt [testenv:warnings] ignore_outcome = True