commit ee7dfa63166bfd01d3d7cf456f2dcb67484032ce Author: Aleksey Date: Fri May 24 09:13:51 2024 +0000 init commit diff --git a/django_filter-24.2.dist-info/INSTALLER b/django_filter-24.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/django_filter-24.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/django_filter-24.2.dist-info/LICENSE b/django_filter-24.2.dist-info/LICENSE new file mode 100644 index 0000000..4b73093 --- /dev/null +++ b/django_filter-24.2.dist-info/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) Alex Gaynor and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The names of its contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/django_filter-24.2.dist-info/METADATA b/django_filter-24.2.dist-info/METADATA new file mode 100644 index 0000000..074aa4c --- /dev/null +++ b/django_filter-24.2.dist-info/METADATA @@ -0,0 +1,150 @@ +Metadata-Version: 2.1 +Name: django-filter +Version: 24.2 +Summary: Django-filter is a reusable Django application for allowing users to filter querysets dynamically. +Author-email: Alex Gaynor +Maintainer-email: Carlton Gibson +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Framework :: Django +Classifier: Framework :: Django :: 4.2 +Classifier: Framework :: Django :: 5.0 +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Requires-Dist: Django>=4.2 +Project-URL: Bug Tracker, https://github.com/carltongibson/django-filter/issues +Project-URL: Changelog, https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst +Project-URL: Documentation, https://django-filter.readthedocs.io/en/main/ +Project-URL: Homepage, https://github.com/carltongibson/django-filter/tree/main +Project-URL: Source Code, https://github.com/carltongibson/django-filter + +Django Filter +============= + +Django-filter is a reusable Django application allowing users to declaratively +add dynamic ``QuerySet`` filtering from URL parameters. + +Full documentation on `read the docs`_. + +.. image:: https://raw.githubusercontent.com/carltongibson/django-filter/python-coverage-comment-action-data/badge.svg + :target: https://github.com/carltongibson/django-filter/tree/python-coverage-comment-action-data + +.. image:: https://badge.fury.io/py/django-filter.svg + :target: http://badge.fury.io/py/django-filter + + +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. + +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 +------------ + +Install using pip: + +.. code-block:: sh + + pip install django-filter + +Then add ``'django_filters'`` to your ``INSTALLED_APPS``. + +.. code-block:: python + + INSTALLED_APPS = [ + ... + 'django_filters', + ] + + +Usage +----- + +Django-filter can be used for generating interfaces similar to the Django +admin's ``list_filter`` interface. It has an API very similar to Django's +``ModelForms``. For example, if you had a Product model you could have a +filterset for it with the code: + +.. code-block:: python + + import django_filters + + class ProductFilter(django_filters.FilterSet): + class Meta: + model = Product + fields = ['name', 'price', 'manufacturer'] + + +And then in your view you could do: + +.. code-block:: python + + def product_list(request): + filter = ProductFilter(request.GET, queryset=Product.objects.all()) + return render(request, 'my_app/template.html', {'filter': filter}) + + +Usage with Django REST Framework +-------------------------------- + +Django-filter provides a custom ``FilterSet`` and filter backend for use with +Django REST Framework. + +To use this adjust your import to use +``django_filters.rest_framework.FilterSet``. + +.. code-block:: python + + from django_filters import rest_framework as filters + + class ProductFilter(filters.FilterSet): + class Meta: + model = Product + fields = ('category', 'in_stock') + + +For more details see the `DRF integration docs`_. + + +Support +------- + +If you need help you can start a `discussion`_. For commercial support, please +`contact Carlton Gibson via his website `_. + +.. _`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_filter-24.2.dist-info/RECORD b/django_filter-24.2.dist-info/RECORD new file mode 100644 index 0000000..0cca409 --- /dev/null +++ b/django_filter-24.2.dist-info/RECORD @@ -0,0 +1,79 @@ +django_filter-24.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +django_filter-24.2.dist-info/LICENSE,sha256=4UQ8qx2nFmTo4lASXOByK3RcVWDurx7_w9HozSy9mAI,1487 +django_filter-24.2.dist-info/METADATA,sha256=hjINlT2OR3cAAhT_hdIFWfqdt6WMx08BPasyr3d-Go0,5120 +django_filter-24.2.dist-info/RECORD,, +django_filter-24.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +django_filter-24.2.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81 +django_filters/__init__.py,sha256=HWgLpY-Ohqbs5SXhmhxMWR9txYylyds8oxmBaTc3gXY,796 +django_filters/__pycache__/__init__.cpython-312.pyc,, +django_filters/__pycache__/compat.cpython-312.pyc,, +django_filters/__pycache__/conf.cpython-312.pyc,, +django_filters/__pycache__/constants.cpython-312.pyc,, +django_filters/__pycache__/exceptions.cpython-312.pyc,, +django_filters/__pycache__/fields.cpython-312.pyc,, +django_filters/__pycache__/filters.cpython-312.pyc,, +django_filters/__pycache__/filterset.cpython-312.pyc,, +django_filters/__pycache__/utils.cpython-312.pyc,, +django_filters/__pycache__/views.cpython-312.pyc,, +django_filters/__pycache__/widgets.cpython-312.pyc,, +django_filters/compat.py,sha256=eatCom2gnXt-HgpU9gyMKqh6Q5ok5RvxdBiL2dFZwas,545 +django_filters/conf.py,sha256=VIxhOioS9Z6qM8PfNaaMtK8A1XSGqvD8tUAH3tmMGkM,3049 +django_filters/constants.py,sha256=N36ZSZJKbcFJIh4DC4Oe7HvkKmYNyRhAHpaQiD5i6SM,63 +django_filters/exceptions.py,sha256=c8EYPU4mY93QAIVGpf7datTHvQt6nrxYJO1SutzFzf4,253 +django_filters/fields.py,sha256=F-iQW7OhtfO4BKe_Jm7bzBRUlcfOut8VxVm19-fGmtg,10373 +django_filters/filters.py,sha256=inMCUojSetasYLF6wBuUv7FGmjOJBQryaCmyo9JBzCU,25326 +django_filters/filterset.py,sha256=iCblMAcHJzl7Wrzv-Ija44t3FJDNoYl_lb81bQ4RnKQ,16239 +django_filters/locale/ar/LC_MESSAGES/django.mo,sha256=utzbP4BsdW91KwGgFwyvXVY1uNZ8otdcUDoZZpIZ9Pg,2568 +django_filters/locale/ar/LC_MESSAGES/django.po,sha256=P-SHUseAhEXgKNQDm2DWB4nFk5Nyh6DAXXSMmBEfx4g,3625 +django_filters/locale/be/LC_MESSAGES/django.mo,sha256=lbp-b9nTHDvBb8ozSkyHWGlmi4X3WyKaObT9GB2fe9E,2819 +django_filters/locale/be/LC_MESSAGES/django.po,sha256=gRsiOMvJ7K8tsa6rOLs2v5ROv5toyFtN6qnh1rppO1c,3696 +django_filters/locale/bg/LC_MESSAGES/django.mo,sha256=ZPmu82dqvj3yd3-J0KLK-hxfwETzqKmq0c-Anozn5Go,2711 +django_filters/locale/bg/LC_MESSAGES/django.po,sha256=zpGSdxLb1erXzUd3GdY6IfC4UlhrbEeNcNKjq3UkjeI,3740 +django_filters/locale/cs/LC_MESSAGES/django.mo,sha256=vZuyiklIF_I3qs9pdhb3OTT2d63aIttgtcHY1b9Gsps,2368 +django_filters/locale/cs/LC_MESSAGES/django.po,sha256=Su0bgXYM0-jA6hDrF_mPLxZSlw0j1waDapguCYnw-Gs,3242 +django_filters/locale/da/LC_MESSAGES/django.mo,sha256=gPy5CaNJWYbCPqeqb6XPr1uynW9FEn8zV_-85RMJaZc,2166 +django_filters/locale/da/LC_MESSAGES/django.po,sha256=mt_ypD4Mt0895YMZWN1bSwkqSI4tAKnPSLizrYD2h3g,3173 +django_filters/locale/de/LC_MESSAGES/django.mo,sha256=IvgqQ0BQ7AiJSmdcGpKWheuLrzrXqs-lbp4Bac2jOdI,2277 +django_filters/locale/de/LC_MESSAGES/django.po,sha256=QFubrkm9Vi0HmoS_5i_JJWGENMiw0MMfn8Kg_FzMYv8,3338 +django_filters/locale/el/LC_MESSAGES/django.mo,sha256=2--juTiXF9v6u95krY9VwZCv2cXoJai6CXi4RWpi39w,2836 +django_filters/locale/el/LC_MESSAGES/django.po,sha256=6tjIPpTNaiJueY3C0N2_vSc_7cY5XkTR2Zl9HWvH15c,3909 +django_filters/locale/es/LC_MESSAGES/django.mo,sha256=5KCl_uUwge5RuGStcyMSsVPD6AOunjNvjuE-32PqWis,2279 +django_filters/locale/es/LC_MESSAGES/django.po,sha256=y-fdqEXzSqbErT58yZYAsAfPdqT0gKShHJZClE3IScc,3426 +django_filters/locale/es_AR/LC_MESSAGES/django.mo,sha256=OCKAVbT3ct5gf2_t5XsKryjlkIQDYZjC67Oz0j-YE6s,703 +django_filters/locale/es_AR/LC_MESSAGES/django.po,sha256=GKRqcNqmulrygz9VxkDRyGS_uG2K0QNT2gyILEcU9BM,3035 +django_filters/locale/fa/LC_MESSAGES/django.mo,sha256=HfEWFF_2l2ypvCJFCzbRf_CKgXlVyzvtVM0sZAl_KQU,2624 +django_filters/locale/fa/LC_MESSAGES/django.po,sha256=n1vb3fQKigB-rUa6oTFJ2Tba7c5qzKRwgHuQn0zJIuM,3623 +django_filters/locale/fi/LC_MESSAGES/django.po,sha256=Odsfeswbdf9hnwZIl24JE36MwOKnnE2yyvcaVzCOvPw,3433 +django_filters/locale/fr/LC_MESSAGES/django.mo,sha256=c87Ugu3u0juDMskRegFA76SkfF5TMi-fexzHb8uWw9w,2344 +django_filters/locale/fr/LC_MESSAGES/django.po,sha256=eF5fMIXDe98C3KMzUOkv7BppBg0YkvuhyLUlylyKnY8,3520 +django_filters/locale/it/LC_MESSAGES/django.mo,sha256=TKIdnZSuYtyCpnl8X9jDyKFuIX6G69CmCvVaWpcuPXM,2268 +django_filters/locale/it/LC_MESSAGES/django.po,sha256=5k7t_TvofiAmV5UlmpAH0t4d-ce7bySWFih0KIxkP_o,3380 +django_filters/locale/nl/LC_MESSAGES/django.mo,sha256=TdtnxLBMuoMY1c0NxZwbGQX3xl4cI7xNP6iBQXpmm6I,2277 +django_filters/locale/nl/LC_MESSAGES/django.po,sha256=ikeFeWaz2_CmWNzNwDHkm-m0Nkuksq9qgQ_oqH_SEeI,3287 +django_filters/locale/pl/LC_MESSAGES/django.mo,sha256=-9taafe4N3mKLdZ4fEXkrj-azO-L4F0fGoxnDgTBuwU,1859 +django_filters/locale/pl/LC_MESSAGES/django.po,sha256=kIM9yYIScAMrQ_W-Pt9DjNPKzeHxBtfOsRgovEBoroU,3720 +django_filters/locale/pt_BR/LC_MESSAGES/django.mo,sha256=GLakV-03XUsCNKaofuG2fGCBIRGVYEMJiC-kD1UX4D0,2263 +django_filters/locale/pt_BR/LC_MESSAGES/django.po,sha256=Qfc9NufXTeQrBLAxbnDZCeRl9TimgQPXTKHnEEiBcdQ,3434 +django_filters/locale/ro/LC_MESSAGES/django.po,sha256=cCCKgqNv1deUxPdV2lRwniApgjPCA9Ft7QYm0rbmJdw,3478 +django_filters/locale/ru/LC_MESSAGES/django.mo,sha256=1KrtkfLhq0BiDskKFffF5i53pM7Tp-bwsbPDe9F4Co0,2796 +django_filters/locale/ru/LC_MESSAGES/django.po,sha256=t6hfrDsO95WwvfKuovZyAmTXz8LIuLULTGmXvfZ6PIQ,3863 +django_filters/locale/sk/LC_MESSAGES/django.mo,sha256=em13cqJIPA3JLTp6JXPXuNNeDqJ7uaEuxxqtOvl9PLk,2394 +django_filters/locale/sk/LC_MESSAGES/django.po,sha256=jg7V3CvkNYjJDMuu9GmfeSj-cC92ja58WdugKsc8GaY,3582 +django_filters/locale/uk/LC_MESSAGES/django.mo,sha256=zgC01vyDPPS81GiD3C4WeQxtCt4_ift_pU-j_2l_LrU,2912 +django_filters/locale/uk/LC_MESSAGES/django.po,sha256=LdEFmgfqczUfYUUdHHbv1SE-ljZ6oqEherQa39mZiwU,3919 +django_filters/locale/zh_CN/LC_MESSAGES/django.mo,sha256=2aSG7Whwpj7iRY_7QcTV-ReuCm8JKsV-ktlRaAbYC0U,852 +django_filters/locale/zh_CN/LC_MESSAGES/django.po,sha256=rogASVeUU81FcYxGClnyXOAhXJc-wBl2AQHYkWUg85E,3354 +django_filters/rest_framework/__init__.py,sha256=HpNAGIdsBRJSkyM1QmqyOTb7I9VVwoMTbexbD21X6vE,113 +django_filters/rest_framework/__pycache__/__init__.cpython-312.pyc,, +django_filters/rest_framework/__pycache__/backends.cpython-312.pyc,, +django_filters/rest_framework/__pycache__/filters.cpython-312.pyc,, +django_filters/rest_framework/__pycache__/filterset.cpython-312.pyc,, +django_filters/rest_framework/backends.py,sha256=2kVwpeH7SRfyO0rQhH9Wq8xY30c3hxuCT2IvVU3ETRc,5744 +django_filters/rest_framework/filters.py,sha256=mh0XhgwhE95HXVOTE3SetP2uCnHjaDmutf3GdThy1l0,312 +django_filters/rest_framework/filterset.py,sha256=3kbngqrt8vg0ckVEXSTeuNhvZbqsk648xS7qGooouxU,1174 +django_filters/templates/django_filters/rest_framework/crispy_form.html,sha256=_Mg40d_4sWAuy7_Mzf1HRACbRgeheu0pGXy2UKpzd3s,108 +django_filters/templates/django_filters/rest_framework/form.html,sha256=KoVGtezI-pWnC18jpCKy3vufR23QLpXXooCgmEFXjAA,211 +django_filters/templates/django_filters/widgets/multiwidget.html,sha256=W0RT7BL9-sF-hCA_Ut4MfWaDwE8Z32syJs3anyurceg,118 +django_filters/utils.py,sha256=ekqKtbEetmY7e3c6FK1fjbLwbmqy2UuYd_faswZW-Tg,11262 +django_filters/views.py,sha256=dZ9uDeCHG7ee_pk1xgchbQp21G9acv84ELBHNBwFs3U,4034 +django_filters/widgets.py,sha256=5DkHm5xcVzzkDO0y6nykx8TqrNCI7Q9V8Ql37yhKRRc,9251 diff --git a/django_filter-24.2.dist-info/REQUESTED b/django_filter-24.2.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/django_filter-24.2.dist-info/WHEEL b/django_filter-24.2.dist-info/WHEEL new file mode 100644 index 0000000..db4a255 --- /dev/null +++ b/django_filter-24.2.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.8.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/django_filters/__init__.py b/django_filters/__init__.py new file mode 100644 index 0000000..ca1f82d --- /dev/null +++ b/django_filters/__init__.py @@ -0,0 +1,39 @@ +# flake8: noqa +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 importlib_util.find_spec("rest_framework"): + from . import rest_framework +del importlib_util + +__version__ = "24.2" + + +def parse_version(version): + """ + '0.1.2.dev1' -> (0, 1, 2, 'dev1') + '0.1.2' -> (0, 1, 2) + """ + v = version.split(".") + ret = [] + for p in v: + if p.isdigit(): + ret.append(int(p)) + else: + 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/__pycache__/__init__.cpython-312.pyc b/django_filters/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ebe5a30 Binary files /dev/null and b/django_filters/__pycache__/__init__.cpython-312.pyc differ diff --git a/django_filters/__pycache__/compat.cpython-312.pyc b/django_filters/__pycache__/compat.cpython-312.pyc new file mode 100644 index 0000000..36c4acd Binary files /dev/null and b/django_filters/__pycache__/compat.cpython-312.pyc differ diff --git a/django_filters/__pycache__/conf.cpython-312.pyc b/django_filters/__pycache__/conf.cpython-312.pyc new file mode 100644 index 0000000..4ef1736 Binary files /dev/null and b/django_filters/__pycache__/conf.cpython-312.pyc differ diff --git a/django_filters/__pycache__/constants.cpython-312.pyc b/django_filters/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000..9cf1a7c Binary files /dev/null and b/django_filters/__pycache__/constants.cpython-312.pyc differ diff --git a/django_filters/__pycache__/exceptions.cpython-312.pyc b/django_filters/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000..b937d9c Binary files /dev/null and b/django_filters/__pycache__/exceptions.cpython-312.pyc differ diff --git a/django_filters/__pycache__/fields.cpython-312.pyc b/django_filters/__pycache__/fields.cpython-312.pyc new file mode 100644 index 0000000..bdceb97 Binary files /dev/null and b/django_filters/__pycache__/fields.cpython-312.pyc differ diff --git a/django_filters/__pycache__/filters.cpython-312.pyc b/django_filters/__pycache__/filters.cpython-312.pyc new file mode 100644 index 0000000..313b82b Binary files /dev/null and b/django_filters/__pycache__/filters.cpython-312.pyc differ diff --git a/django_filters/__pycache__/filterset.cpython-312.pyc b/django_filters/__pycache__/filterset.cpython-312.pyc new file mode 100644 index 0000000..f9addc1 Binary files /dev/null and b/django_filters/__pycache__/filterset.cpython-312.pyc differ diff --git a/django_filters/__pycache__/utils.cpython-312.pyc b/django_filters/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..2658476 Binary files /dev/null and b/django_filters/__pycache__/utils.cpython-312.pyc differ diff --git a/django_filters/__pycache__/views.cpython-312.pyc b/django_filters/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..1fa5d1d Binary files /dev/null and b/django_filters/__pycache__/views.cpython-312.pyc differ diff --git a/django_filters/__pycache__/widgets.cpython-312.pyc b/django_filters/__pycache__/widgets.cpython-312.pyc new file mode 100644 index 0000000..79d5b6e Binary files /dev/null and b/django_filters/__pycache__/widgets.cpython-312.pyc differ diff --git a/django_filters/compat.py b/django_filters/compat.py new file mode 100644 index 0000000..c07f8a3 --- /dev/null +++ b/django_filters/compat.py @@ -0,0 +1,25 @@ +from django.conf import settings + +# django-crispy-forms is optional +try: + import crispy_forms +except ImportError: + crispy_forms = None + + +def is_crispy(): + return "crispy_forms" in settings.INSTALLED_APPS and crispy_forms + + +# coreapi is optional (Note that uritemplate is a dependency of coreapi) +# Fixes #525 - cannot simply import from rest_framework.compat, due to +# import issues w/ django-guardian. +try: + import coreapi +except ImportError: + coreapi = None + +try: + import coreschema +except ImportError: + coreschema = None diff --git a/django_filters/conf.py b/django_filters/conf.py new file mode 100644 index 0000000..cbef0b7 --- /dev/null +++ b/django_filters/conf.py @@ -0,0 +1,102 @@ +from django.conf import settings as dj_settings +from django.core.signals import setting_changed +from django.utils.translation import gettext_lazy as _ + +from .utils import deprecate + +DEFAULTS = { + "DISABLE_HELP_TEXT": False, + "DEFAULT_LOOKUP_EXPR": "exact", + # empty/null choices + "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"), + # 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": _("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"), + }, +} + + +DEPRECATED_SETTINGS = [] + + +def is_callable(value): + # check for callables, except types + return callable(value) and not isinstance(value, type) + + +class Settings: + def __getattr__(self, name): + if name not in DEFAULTS: + msg = "'%s' object has no attribute '%s'" + raise AttributeError(msg % (self.__class__.__name__, name)) + + value = self.get_setting(name) + + if is_callable(value): + value = value() + + # Cache the result + setattr(self, name, value) + return value + + def get_setting(self, 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) + + return getattr(dj_settings, django_setting, DEFAULTS[setting]) + + def change_setting(self, setting, value, enter, **kwargs): + if not setting.startswith("FILTERS_"): + return + setting = setting[8:] # strip 'FILTERS_' + + # ensure a valid app setting is being overridden + if setting not in DEFAULTS: + return + + # if exiting, delete value to repopulate + if enter: + setattr(self, setting, value) + else: + delattr(self, setting) + + +settings = Settings() +setting_changed.connect(settings.change_setting) diff --git a/django_filters/constants.py b/django_filters/constants.py new file mode 100644 index 0000000..93aa5b3 --- /dev/null +++ b/django_filters/constants.py @@ -0,0 +1,4 @@ +ALL_FIELDS = "__all__" + + +EMPTY_VALUES = ([], (), {}, "", None) diff --git a/django_filters/exceptions.py b/django_filters/exceptions.py new file mode 100644 index 0000000..978bfb3 --- /dev/null +++ b/django_filters/exceptions.py @@ -0,0 +1,8 @@ +from django.core.exceptions import FieldError + + +class FieldLookupError(FieldError): + def __init__(self, model_field, lookup_expr): + super().__init__( + "Unsupported lookup '%s' for field '%s'." % (lookup_expr, model_field) + ) diff --git a/django_filters/fields.py b/django_filters/fields.py new file mode 100644 index 0000000..d6ac79c --- /dev/null +++ b/django_filters/fields.py @@ -0,0 +1,324 @@ +from collections import namedtuple +from datetime import datetime, time + +from django import forms +from django.utils.dateparse import parse_datetime +from django.utils.encoding import force_str +from django.utils.translation import gettext_lazy as _ + +from .conf import settings +from .constants import EMPTY_VALUES +from .utils import handle_timezone +from .widgets import ( + BaseCSVWidget, + CSVWidget, + DateRangeWidget, + LookupChoiceWidget, + 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()) + super().__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if data_list: + return slice(*data_list) + return None + + +class DateRangeField(RangeField): + widget = DateRangeWidget + + def __init__(self, *args, **kwargs): + fields = (forms.DateField(), forms.DateField()) + super().__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if data_list: + start_date, stop_date = data_list + if start_date: + start_date = handle_timezone( + datetime.combine(start_date, time.min), False + ) + if stop_date: + stop_date = handle_timezone( + datetime.combine(stop_date, time.max), False + ) + return slice(start_date, stop_date) + return None + + +class DateTimeRangeField(RangeField): + widget = DateRangeWidget + + def __init__(self, *args, **kwargs): + fields = (forms.DateTimeField(), forms.DateTimeField()) + super().__init__(fields, *args, **kwargs) + + +class IsoDateTimeRangeField(RangeField): + widget = DateRangeWidget + + def __init__(self, *args, **kwargs): + fields = (IsoDateTimeField(), IsoDateTimeField()) + super().__init__(fields, *args, **kwargs) + + +class TimeRangeField(RangeField): + widget = DateRangeWidget + + def __init__(self, *args, **kwargs): + fields = (forms.TimeField(), forms.TimeField()) + super().__init__(fields, *args, **kwargs) + + +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( + "Empty values ([], (), {}, '', None) are not " + "valid Lookup arguments. Return None instead." + ) + + return super().__new__(cls, value, lookup_expr) + + +class LookupChoiceField(forms.MultiValueField): + default_error_messages = { + "lookup_required": _("Select a lookup."), + } + + def __init__(self, field, lookup_choices, *args, **kwargs): + 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 + super().__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if len(data_list) == 2: + value, lookup_expr = data_list + if value not in EMPTY_VALUES: + if lookup_expr not in EMPTY_VALUES: + return Lookup(value=value, lookup_expr=lookup_expr) + else: + raise forms.ValidationError( + self.error_messages["lookup_required"], code="lookup_required" + ) + return None + + +class IsoDateTimeField(forms.DateTimeField): + """ + Supports 'iso-8601' date format too which is out the scope of + the ``datetime.strptime`` standard library + + # ISO 8601: ``http://www.w3.org/TR/NOTE-datetime`` + + Based on Gist example by David Medina https://gist.github.com/copitux/5773821 + """ + + ISO_8601 = "iso-8601" + input_formats = [ISO_8601] + + def strptime(self, value, format): + value = force_str(value) + + if format == self.ISO_8601: + parsed = parse_datetime(value) + if parsed is None: # Continue with other formats if doesn't match + raise ValueError + return handle_timezone(parsed) + return super().strptime(value, format) + + +class BaseCSVField(forms.Field): + """ + Base field for validating CSV types. Value validation is performed by + secondary base classes. + + ex:: + 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) + + 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) + ): + 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, {}) + + 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] + + +class BaseRangeField(BaseCSVField): + # Force use of text input, as range must always have two inputs. A date + # 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.")} + + def clean(self, value): + value = super().clean(value) + + assert value is None or isinstance(value, list) + + if value and len(value) != 2: + raise forms.ValidationError( + self.error_messages["invalid_values"], code="invalid_values" + ) + + return value + + +class ChoiceIterator(BaseChoiceIterator if DJANGO_50 else object): + # Emulates the behavior of ModelChoiceIterator, but instead wraps + # the field's _choices iterable. + + def __init__(self, field, choices): + self.field = field + self.choices = choices + + def __iter__(self): + if self.field.empty_label is not None: + yield ("", self.field.empty_label) + if self.field.null_label is not None: + yield (self.field.null_value, self.field.null_label) + 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 + add += 1 if self.field.null_label is not None else 0 + return len(self.choices) + add + + +class ModelChoiceIterator(forms.models.ModelChoiceIterator): + # Extends the base ModelChoiceIterator to add in 'null' choice handling. + # This is a bit verbose since we have to insert the null choice after the + # empty choice, but before the remainder of the choices. + + def __iter__(self): + iterable = super().__iter__() + + if self.field.empty_label is not None: + yield next(iterable) + if self.field.null_label is not None: + yield (self.field.null_value, self.field.null_label) + yield from iterable + + def __len__(self): + add = 1 if self.field.null_label is not None else 0 + return super().__len__() + add + + +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) + + super().__init__(*args, **kwargs) + + @property + def choices(self): + return super().choices + + @choices.setter + def choices(self, value): + if DJANGO_50: + value = self.iterator(self, value) + # 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) + else: + super()._set_choices(value) + value = self.iterator(self, self._choices) + self._choices = self.widget.choices = value + + +# Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label +class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField): + iterator = ChoiceIterator + + def __init__(self, *args, **kwargs): + self.empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL) + super().__init__(*args, **kwargs) + + +class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField): + iterator = ChoiceIterator + + def __init__(self, *args, **kwargs): + self.empty_label = None + super().__init__(*args, **kwargs) + + +class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField): + iterator = ModelChoiceIterator + + def to_python(self, value): + # bypass the queryset value check + if self.null_label is not None and value == self.null_value: + return value + return super().to_python(value) + + +class ModelMultipleChoiceField(ChoiceIteratorMixin, forms.ModelMultipleChoiceField): + iterator = ModelChoiceIterator + + def _check_values(self, value): + null = self.null_label is not None and value and self.null_value in value + if null: # remove the null value and any potential duplicates + value = [v for v in value if v != self.null_value] + + result = list(super()._check_values(value)) + result += [self.null_value] if null else [] + return result diff --git a/django_filters/filters.py b/django_filters/filters.py new file mode 100644 index 0000000..a024e9b --- /dev/null +++ b/django_filters/filters.py @@ -0,0 +1,852 @@ +from collections import OrderedDict +from collections.abc import Iterable +from datetime import timedelta +from itertools import chain + +from django import forms +from django.core.validators import MaxValueValidator +from django.db.models import Q +from django.db.models.constants import LOOKUP_SEP +from django.forms.utils import pretty_name +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ + +from .conf import settings +from .constants import EMPTY_VALUES +from .fields import ( + BaseCSVField, + BaseRangeField, + ChoiceField, + DateRangeField, + DateTimeRangeField, + IsoDateTimeField, + IsoDateTimeRangeField, + LookupChoiceField, + ModelChoiceField, + ModelMultipleChoiceField, + MultipleChoiceField, + RangeField, + 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", +] + + +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 + ): + if lookup_expr is None: + lookup_expr = settings.DEFAULT_LOOKUP_EXPR + self.field_name = field_name + self.lookup_expr = lookup_expr + self.label = label + self.method = method + self.distinct = distinct + self.exclude = exclude + + self.extra = kwargs + 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. + """ + return qs.exclude if self.exclude else qs.filter + + def method(): + """ + Filter method needs to be lazily resolved, as it may be dependent on + the 'parent' FilterSet. + """ + + def fget(self): + return self._method + + def fset(self, value): + self._method = value + + # clear existing FilterMethod + if isinstance(self.filter, FilterMethod): + del self.filter + + # override filter w/ FilterMethod. + if value is not None: + self.filter = FilterMethod(self) + + return locals() + + method = property(**method()) + + def label(): + def fget(self): + if self._label is None and hasattr(self, "model"): + self._label = label_for_filter( + self.model, self.field_name, self.lookup_expr, self.exclude + ) + return self._label + + def fset(self, value): + self._label = value + + return locals() + + label = property(**label()) + + @property + def field(self): + if not hasattr(self, "_field"): + field_kwargs = self.extra.copy() + + if settings.DISABLE_HELP_TEXT: + field_kwargs.pop("help_text", None) + + self._field = self.field_class(label=self.label, **field_kwargs) + return self._field + + def filter(self, qs, value): + if value in EMPTY_VALUES: + return qs + if self.distinct: + qs = qs.distinct() + lookup = "%s__%s" % (self.field_name, self.lookup_expr) + qs = self.get_method(qs)(**{lookup: value}) + return qs + + +class CharFilter(Filter): + field_class = forms.CharField + + +class BooleanFilter(Filter): + field_class = forms.NullBooleanField + + +class ChoiceFilter(Filter): + field_class = ChoiceField + + def __init__(self, *args, **kwargs): + 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} + ) + return qs.distinct() if self.distinct else qs + + +class TypedChoiceFilter(Filter): + field_class = forms.TypedChoiceField + + +class UUIDFilter(Filter): + field_class = forms.UUIDField + + +class MultipleChoiceFilter(Filter): + """ + This filter performs OR(by default) or AND(using conjoined=True) query + on the selected options. + + Advanced usage + -------------- + Depending on your application logic, when all or no choices are selected, + filtering may be a no-operation. In this case you may wish to avoid the + filtering overhead, particularly if using a `distinct` call. + + You can override `get_filter_predicate` to use a custom filter. + By default it will use the filter's name for the key, and the value will + be the model object - or in case of passing in `to_field_name` the + value of that attribute on the model. + + Set `always_filter` to `False` after instantiation to enable the default + `is_noop` test. You can override `is_noop` if you need a different test + for your application. + + `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) + super().__init__(*args, **kwargs) + + def is_noop(self, qs, value): + """ + Return `True` to short-circuit unnecessary and potentially slow + filtering. + """ + if self.always_filter: + return False + + # A reasonable default for being a noop... + if self.extra.get("required") and len(value) == len(self.field.choices): + return True + + return False + + def filter(self, qs, value): + if not value: + # Even though not a noop, no point filtering if empty. + return qs + + if self.is_noop(qs, value): + return qs + + if not self.conjoined: + q = Q() + for v in set(value): + if v == self.null_value: + v = None + predicate = self.get_filter_predicate(v) + if self.conjoined: + qs = self.get_method(qs)(**predicate) + else: + q |= Q(**predicate) + + if not self.conjoined: + qs = self.get_method(qs)(q) + + return qs.distinct() if self.distinct else qs + + def get_filter_predicate(self, v): + name = self.field_name + if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR: + name = LOOKUP_SEP.join([name, self.lookup_expr]) + try: + return {name: getattr(v, self.field.to_field_name)} + except (AttributeError, TypeError): + return {name: v} + + +class TypedMultipleChoiceFilter(MultipleChoiceFilter): + field_class = forms.TypedMultipleChoiceField + + +class DateFilter(Filter): + field_class = forms.DateField + + +class DateTimeFilter(Filter): + field_class = forms.DateTimeField + + +class IsoDateTimeFilter(DateTimeFilter): + """ + Uses IsoDateTimeField to support filtering on ISO 8601 formatted datetimes. + + For context see: + + * https://code.djangoproject.com/ticket/23448 + * https://github.com/encode/django-rest-framework/issues/1338 + * https://github.com/carltongibson/django-filter/pull/264 + """ + + field_class = IsoDateTimeField + + +class TimeFilter(Filter): + field_class = forms.TimeField + + +class DurationFilter(Filter): + field_class = forms.DurationField + + +class QuerySetRequestMixin: + """ + Add callable functionality to filters that support the ``queryset`` + argument. If the ``queryset`` is callable, then it **must** accept the + ``request`` object as a single argument. + + This is useful for filtering querysets by properties on the ``request`` + object, such as the user. + + Example:: + + def departments(request): + company = request.user.company + return company.department_set.all() + + class EmployeeFilter(filters.FilterSet): + department = filters.ModelChoiceFilter(queryset=departments) + ... + + The above example restricts the set of departments to those in the logged-in + user's associated company. + + """ + + def __init__(self, *args, **kwargs): + self.queryset = kwargs.get("queryset") + super().__init__(*args, **kwargs) + + def get_request(self): + try: + return self.parent.request + except AttributeError: + return None + + def get_queryset(self, request): + queryset = self.queryset + + if callable(queryset): + return queryset(request) + return queryset + + @property + def field(self): + request = self.get_request() + queryset = self.get_queryset(request) + + if queryset is not None: + self.extra["queryset"] = queryset + + return super().field + + +class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter): + field_class = ModelChoiceField + + def __init__(self, *args, **kwargs): + kwargs.setdefault("empty_label", settings.EMPTY_CHOICE_LABEL) + super().__init__(*args, **kwargs) + + +class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter): + field_class = ModelMultipleChoiceField + + +class NumberFilter(Filter): + field_class = forms.DecimalField + + def get_max_validator(self): + """ + Return a MaxValueValidator for the field, or None to disable. + """ + return MaxValueValidator(1e50) + + @property + def field(self): + if not hasattr(self, "_field"): + field = super().field + max_validator = self.get_max_validator() + if max_validator: + field.validators.append(max_validator) + + self._field = field + return self._field + + +class NumericRangeFilter(Filter): + field_class = RangeField + + def filter(self, qs, value): + if 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" + value = value.start + elif value.stop is not None: + self.lookup_expr = "endswith" + value = value.stop + + return super().filter(qs, value) + + +class RangeFilter(Filter): + field_class = RangeField + + def filter(self, qs, value): + if value: + if value.start is not None and value.stop is not None: + self.lookup_expr = "range" + value = (value.start, value.stop) + elif value.start is not None: + self.lookup_expr = "gte" + value = value.start + elif value.stop is not None: + self.lookup_expr = "lte" + value = value.stop + + return super().filter(qs, value) + + +def _truncate(dt): + return dt.date() + + +class DateRangeFilter(ChoiceFilter): + choices = [ + ("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, + } + ), + } + + def __init__(self, choices=None, filters=None, *args, **kwargs): + if choices is not None: + self.choices = choices + if filters is not None: + self.filters = filters + + 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) + super().__init__(choices=self.choices, *args, **kwargs) + + def filter(self, qs, value): + if not value: + return qs + + assert value in self.filters + + qs = self.filters[value](qs, self.field_name) + return qs.distinct() if self.distinct else qs + + +class DateFromToRangeFilter(RangeFilter): + field_class = DateRangeField + + +class DateTimeFromToRangeFilter(RangeFilter): + field_class = DateTimeRangeField + + +class IsoDateTimeFromToRangeFilter(RangeFilter): + field_class = IsoDateTimeRangeField + + +class TimeRangeFilter(RangeFilter): + field_class = TimeRangeField + + +class AllValuesFilter(ChoiceFilter): + @property + 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] + return super().field + + +class AllValuesMultipleFilter(MultipleChoiceFilter): + @property + 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] + return super().field + + +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.")) + 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 + ) + + self.field_class = ConcreteCSVField + + @classmethod + def _field_class_name(cls, field_class, lookup_expr): + """ + Generate a suitable class name for the concrete field class. This is not + completely reliable, as not all field class names are of the format + Field. + + ex:: + + BaseCSVFilter._field_class_name(DateTimeField, 'year__in') + + returns 'DateTimeYearInField' + + """ + # DateTimeField => DateTime + type_name = field_class.__name__ + 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) + + # DateTimeYearInField + return str("%s%sField" % (type_name, expression_name)) + + +class BaseInFilter(BaseCSVFilter): + def __init__(self, *args, **kwargs): + kwargs.setdefault("lookup_expr", "in") + super().__init__(*args, **kwargs) + + +class BaseRangeFilter(BaseCSVFilter): + base_field_class = BaseRangeField + + def __init__(self, *args, **kwargs): + kwargs.setdefault("lookup_expr", "range") + super().__init__(*args, **kwargs) + + +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 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 + form field class used to validate the value. Default: ``forms.CharField`` + + ex:: + + price = django_filters.LookupChoiceFilter( + field_class=forms.DecimalField, + lookup_choices=[ + ('exact', 'Equals'), + ('gt', 'Greater than'), + ('lt', 'Less than'), + ] + ) + + """ + + 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) + + super(LookupChoiceFilter, self).__init__(field_name=field_name, **kwargs) + + self.lookup_choices = lookup_choices + if field_class is not None: + self.field_class = field_class + + @classmethod + def normalize_lookup(cls, lookup): + """ + Normalize the lookup into a tuple of ``(lookup expression, display value)`` + + If the ``lookup`` is already a tuple, the tuple is not altered. + If the ``lookup`` is a string, a tuple is returned with the lookup + expression used as the basis for the display value. + + ex:: + + >>> LookupChoiceFilter.normalize_lookup(('exact', 'Equals')) + ('exact', 'Equals') + + >>> LookupChoiceFilter.normalize_lookup('has_key') + ('has_key', 'Has key') + + """ + if isinstance(lookup, str): + return (lookup, pretty_name(lookup)) + return (lookup[0], lookup[1]) + + def get_lookup_choices(self): + """ + Get the lookup choices in a format suitable for ``django.forms.ChoiceField``. + If the filter is initialized with ``lookup_choices``, this value is normalized + and passed to the underlying ``LookupChoiceField``. If no choices are provided, + they are generated from the corresponding model field's registered lookups. + """ + lookups = self.lookup_choices + if lookups is None: + field = get_model_field(self.model, self.field_name) + lookups = field.get_lookups() + + return [self.normalize_lookup(lookup) for lookup in lookups] + + @property + def field(self): + if not hasattr(self, "_field"): + inner_field = super().field + lookups = self.get_lookup_choices() + + self._field = self.outer_class( + inner_field, + lookups, + label=self.label, + empty_label=self.empty_label, + required=self.extra["required"], + ) + + return self._field + + def filter(self, qs, lookup): + if not lookup: + return super().filter(qs, None) + + self.lookup_expr = lookup.lookup_expr + return super().filter(qs, lookup.value) + + +class OrderingFilter(BaseCSVFilter, ChoiceFilter): + """ + Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts + two additional arguments that are used to build the ordering choices. + + * ``fields`` is a mapping of {model field name: parameter name}. The + parameter names are exposed in the choices and mask/alias the field + names used in the ``order_by()`` call. Similar to field ``choices``, + ``fields`` accepts the 'list of two-tuples' syntax that retains order. + ``fields`` may also just be an iterable of strings. In this case, the + field names simply double as the exposed parameter names. + + * ``field_labels`` is an optional argument that allows you to customize + the display label for the corresponding parameter. It accepts a mapping + of {field name: human readable label}. Keep in mind that the key is the + field name, and not the exposed parameter name. + + Additionally, you can just provide your own ``choices`` if you require + explicit control over the exposed options. For example, when you might + want to disable descending sort options. + + This filter is also CSV-based, and accepts multiple ordering params. The + default select widget does not enable the use of this, but it is useful + for APIs. + + """ + + 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 = self.normalize_fields(fields) + 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) + + 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("-") + param = param[1:] if descending else param + field_name = self.param_map.get(param, param) + + return "-%s" % field_name if descending else field_name + + def filter(self, qs, value): + if value in EMPTY_VALUES: + return qs + + ordering = [ + self.get_ordering_value(param) + for param in value + if param not in EMPTY_VALUES + ] + return qs.order_by(*ordering) + + @classmethod + def normalize_fields(cls, fields): + """ + Normalize the fields into an ordered map of {field name: param name} + """ + # fields is a mapping, copy into new OrderedDict + if isinstance(fields, dict): + return OrderedDict(fields) + + # convert iterable of values => iterable of pairs (field name, param name) + assert isinstance( + fields, Iterable + ), "'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 isinstance(field, Iterable) + 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]) + + def build_choices(self, fields, labels): + ascending = [ + (param, labels.get(field, _(pretty_name(param)))) + for field, param in fields.items() + ] + descending = [ + ("-%s" % param, labels.get("-%s" % param, self.descending_fmt % label)) + for param, label in ascending + ] + + # interleave the ascending and descending choices + return [val for pair in zip(ascending, descending) for val in pair] + + +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 + + def __call__(self, qs, value): + if value in EMPTY_VALUES: + return qs + + return self.method(qs, self.f.field_name, value) + + @property + def method(self): + """ + Resolve the method on the parent filterset. + """ + instance = self.f + + # noop if 'method' is a function + if callable(instance.method): + 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, + ) + + 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, + ) + + return method diff --git a/django_filters/filterset.py b/django_filters/filterset.py new file mode 100644 index 0000000..6c7019e --- /dev/null +++ b/django_filters/filterset.py @@ -0,0 +1,473 @@ +import copy +from collections import OrderedDict + +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.utils.datastructures import MultiValueDict + +from .conf import settings +from .constants import ALL_FIELDS +from .filters import ( + BaseInFilter, + BaseRangeFilter, + BooleanFilter, + CharFilter, + ChoiceFilter, + DateFilter, + DateTimeFilter, + DurationFilter, + Filter, + ModelChoiceFilter, + ModelMultipleChoiceFilter, + NumberFilter, + TimeFilter, + UUIDFilter, +) +from .utils import get_all_model_fields, get_model_field, resolve_field, try_dbfield + + +def remote_queryset(field): + """ + Get the queryset for the other side of a relationship. This works + for both `RelatedField`s and `ForeignObjectRel`s. + """ + model = field.related_model + + # Reverse relationships do not have choice limits + if not hasattr(field, "get_limit_choices_to"): + return model._default_manager.all() + + limit_choices_to = field.get_limit_choices_to() + return model._default_manager.complex_filter(limit_choices_to) + + +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.filter_overrides = getattr(options, "filter_overrides", {}) + + 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) + + new_class = super().__new__(cls, name, bases, attrs) + new_class._meta = FilterSetOptions(getattr(new_class, "Meta", None)) + new_class.base_filters = new_class.get_filters() + + return new_class + + @classmethod + def get_declared_filters(cls, bases, attrs): + filters = [ + (filter_name, attrs.pop(filter_name)) + for filter_name, obj in list(attrs.items()) + if isinstance(obj, Filter) + ] + + # 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: + f.field_name = filter_name + + filters.sort(key=lambda x: x[1].creation_counter) + + # Ensures a base class field doesn't override cls attrs, and maintains + # field precedence when inheriting multiple parents. e.g. if there is a + # class C(A, B), and A and B both define 'field', use 'field' from A. + known = set(attrs) + + def visit(name): + known.add(name) + return name + + base_filters = [ + (visit(name), f) + for base in bases + if hasattr(base, "declared_filters") + for name, f in base.declared_filters.items() + if name not in known + ] + + return OrderedDict(base_filters + filters) + + +FILTER_FOR_DBFIELD_DEFAULTS = { + 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, + }, + }, + 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, + }, + }, + models.ManyToManyField: { + "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, + }, + }, + ManyToOneRel: { + "filter_class": ModelMultipleChoiceFilter, + "extra": lambda f: { + "queryset": remote_queryset(f), + }, + }, + ManyToManyRel: { + "filter_class": ModelMultipleChoiceFilter, + "extra": lambda f: { + "queryset": remote_queryset(f), + }, + }, +} + + +class BaseFilterSet: + FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS + + def __init__(self, data=None, queryset=None, *, request=None, prefix=None): + if queryset is None: + queryset = self._meta.model._default_manager.all() + model = queryset.model + + self.is_bound = data is not None + self.data = data or MultiValueDict() + self.queryset = queryset + self.request = request + self.form_prefix = prefix + + self.filters = copy.deepcopy(self.base_filters) + + # propagate the model and filterset to the filters + for filter_ in self.filters.values(): + filter_.model = model + filter_.parent = self + + def is_valid(self): + """ + Return True if the underlying form has no errors, or False otherwise. + """ + return self.is_bound and self.form.is_valid() + + @property + def errors(self): + """ + Return an ErrorDict for the data provided for the underlying form. + """ + return self.form.errors + + def filter_queryset(self, queryset): + """ + Filter the queryset with the underlying form's `cleaned_data`. You must + call `is_valid()` or `errors` before calling this method. + + This method should be overridden if additional filtering needs to be + applied to the queryset before it is cached. + """ + 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__, + ) + return queryset + + @property + def qs(self): + if not hasattr(self, "_qs"): + qs = self.queryset.all() + if self.is_bound: + # ensure form validation before filtering + self.errors + qs = self.filter_queryset(qs) + self._qs = qs + return self._qs + + def get_form_class(self): + """ + Returns a django Form suitable of validating the filterset data. + + 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()] + ) + + return type(str("%sForm" % self.__class__.__name__), (self._meta.form,), fields) + + @property + def form(self): + if not hasattr(self, "_form"): + Form = self.get_form_class() + if self.is_bound: + self._form = Form(self.data, prefix=self.form_prefix) + else: + self._form = Form(prefix=self.form_prefix) + return self._form + + @classmethod + def get_fields(cls): + """ + Resolve the 'fields' argument that should be used for generating filters on the + filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'. + """ + model = cls._meta.model + 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 " + "'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: + fields = ALL_FIELDS + + # Resolve ALL_FIELDS into all fields for the filterset's model. + if fields == ALL_FIELDS: + fields = get_all_model_fields(model) + + # 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 + ] + else: + fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude] + + return OrderedDict(fields) + + @classmethod + def get_filter_name(cls, field_name, lookup_expr): + """ + Combine a field name and lookup expression into a usable filter name. + Exact lookups are the implicit default, so "exact" is stripped from the + end of the filter name. + """ + filter_name = LOOKUP_SEP.join([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)] + + return filter_name + + @classmethod + def get_filters(cls): + """ + Get all filters for the filterset. This is the combination of declared and + generated filters. + """ + + # No model specified - skip filter generation + if not cls._meta.model: + return cls.declared_filters.copy() + + # Determine the filters that should be included on the filterset. + filters = OrderedDict() + fields = cls.get_fields() + undefined = [] + + for field_name, lookups in fields.items(): + field = get_model_field(cls._meta.model, field_name) + + # warn if the field doesn't exist. + if field is None: + undefined.append(field_name) + + for lookup_expr in lookups: + filter_name = cls.get_filter_name(field_name, lookup_expr) + + # If the filter is explicitly declared on the class, skip generation + if filter_name in cls.declared_filters: + filters[filter_name] = cls.declared_filters[filter_name] + continue + + if field is not None: + 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)): + undefined = [f for f in undefined if f not in cls.declared_filters] + + if undefined: + raise TypeError( + "'Meta.fields' must not contain non-model field names: %s" + % ", ".join(undefined) + ) + + # Add in declared filters. This is necessary since we don't enforce adding + # declared filters to the 'Meta.fields' option + filters.update(cls.declared_filters) + return filters + + @classmethod + def filter_for_field(cls, field, field_name, lookup_expr=None): + if lookup_expr is None: + lookup_expr = settings.DEFAULT_LOOKUP_EXPR + field, lookup_type = resolve_field(field, lookup_expr) + + default = { + "field_name": field_name, + "lookup_expr": lookup_expr, + } + + filter_class, params = cls.filter_for_lookup(field, lookup_type) + default.update(params) + + 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/main/ref/filterset.html" + "#customise-filter-generation-with-filter-overrides" + ) % (cls.__name__, field_name, lookup_expr, field.__class__.__name__) + + return filter_class(**default) + + @classmethod + def filter_for_lookup(cls, field, lookup_type): + DEFAULTS = dict(cls.FILTER_DEFAULTS) + 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) + + # 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 == "isnull": + data = try_dbfield(DEFAULTS.get, models.BooleanField) + + filter_class = data.get("filter_class") + params = data.get("extra", lambda field: {})(field) + return filter_class, params + + 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": + + class ConcreteRangeFilter(BaseRangeFilter, filter_class): + pass + + ConcreteRangeFilter.__name__ = cls._csv_filter_class_name( + filter_class, lookup_type + ) + + return ConcreteRangeFilter, params + + return filter_class, params + + @classmethod + def _csv_filter_class_name(cls, filter_class, lookup_type): + """ + Generate a suitable class name for a concrete filter class. This is not + completely reliable, as not all filter class names are of the format + Filter. + + ex:: + + FilterSet._csv_filter_class_name(DateTimeFilter, 'in') + + returns 'DateTimeInFilter' + + """ + # DateTimeFilter => DateTime + type_name = filter_class.__name__ + 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)) + + +class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass): + pass + + +def filterset_factory(model, filterset=FilterSet, fields=None): + attrs = {"model": model} + if fields is None: + if getattr(getattr(filterset, "Meta", {}), "fields", None) is None: + attrs["fields"] = ALL_FIELDS + else: + attrs["fields"] = fields + bases = (filterset.Meta,) if hasattr(filterset, "Meta") else () + Meta = type("Meta", bases, attrs) + return type(filterset)( + str("%sFilterSet" % model._meta.object_name), (filterset,), {"Meta": Meta} + ) 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 0000000..b1b876f 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 0000000..9e5caee --- /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 new file mode 100644 index 0000000..595dad9 Binary files /dev/null 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 new file mode 100644 index 0000000..84ab342 --- /dev/null +++ b/django_filters/locale/be/LC_MESSAGES/django.po @@ -0,0 +1,191 @@ +# +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: 2016-09-29 11:47+0300\n" +"Last-Translator: Eugena Mikhaylikova \n" +"Language-Team: TextTempearture\n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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: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 "Мінулыя 7 дзён" + +#: 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 "Няма" + +#~ msgid "Any date" +#~ msgstr "Любая дата" diff --git a/django_filters/locale/bg/LC_MESSAGES/django.mo b/django_filters/locale/bg/LC_MESSAGES/django.mo new file mode 100644 index 0000000..122fe4d Binary files /dev/null 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 new file mode 100644 index 0000000..476cc12 --- /dev/null +++ b/django_filters/locale/bg/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. +# Hristo Gatsinski , 2019. +# +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: 2019-12-21 19:36+0200\n" +"Last-Translator: Hristo Gatsinski \n" +"Language-Team: \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.8.9\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 "Последните 7 дни" + +#: 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/cs/LC_MESSAGES/django.mo b/django_filters/locale/cs/LC_MESSAGES/django.mo new file mode 100644 index 0000000..54a8915 Binary files /dev/null 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 new file mode 100644 index 0000000..36d3d88 --- /dev/null +++ b/django_filters/locale/cs/LC_MESSAGES/django.po @@ -0,0 +1,190 @@ +# +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: 2016-09-29 11:47+0300\n" +"Last-Translator: Eugena Mikhaylikova \n" +"Language-Team: TextTempearture\n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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:16 +msgid "date" +msgstr "datum" + +#: conf.py:17 +msgid "year" +msgstr "rok" + +#: conf.py:18 +msgid "month" +msgstr "měsíc" + +#: conf.py:19 +msgid "day" +msgstr "den" + +#: conf.py:20 +msgid "week day" +msgstr "den v týdnu" + +#: conf.py:21 +msgid "hour" +msgstr "hodinu" + +#: conf.py:22 +msgid "minute" +msgstr "minutu" + +#: conf.py:23 +msgid "second" +msgstr "vteřina" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "obsahuje" + +#: conf.py:29 +msgid "is in" +msgstr "v" + +#: conf.py:30 +msgid "is greater than" +msgstr "více než" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "větší nebo roven" + +#: conf.py:32 +msgid "is less than" +msgstr "méně než" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "menší nebo rovné" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "začíná" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "končí" + +#: conf.py:38 +msgid "is in range" +msgstr "v rozsahu" + +#: 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:42 conf.py:49 +msgid "search" +msgstr "vyhledávání" + +#: conf.py:44 +msgid "is contained by" +msgstr "je obsažen v" + +#: conf.py:45 +msgid "overlaps" +msgstr "překrývají" + +#: conf.py:46 +msgid "has key" +msgstr "má klíč" + +#: conf.py:47 +msgid "has keys" +msgstr "má klíče" + +#: conf.py:48 +msgid "has any keys" +msgstr "má nějaké klíče" + +#: 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:437 +msgid "Today" +msgstr "Dnes" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Včera" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Posledních 7 dní" + +#: filters.py:440 +msgid "This month" +msgstr "Tento měsíc" + +#: filters.py:441 +msgid "This year" +msgstr "Tento rok" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Více hodnot lze oddělit čárkami." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (sestupně)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Řád z" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Odeslat" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Filtry na polích" + +#: utils.py:308 +msgid "exclude" +msgstr "s výjimkou" + +#: widgets.py:58 +msgid "All" +msgstr "Všechno" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Není nastaveno" + +#: widgets.py:162 +msgid "Yes" +msgstr "Ano" + +#: 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 new file mode 100644 index 0000000..84f7e94 Binary files /dev/null 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 new file mode 100644 index 0000000..de07d28 --- /dev/null +++ b/django_filters/locale/da/LC_MESSAGES/django.po @@ -0,0 +1,190 @@ +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" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: conf.py:16 +msgid "date" +msgstr "dato" + +#: conf.py:17 +msgid "year" +msgstr "år" + +#: conf.py:18 +msgid "month" +msgstr "måned" + +#: conf.py:19 +msgid "day" +msgstr "dag" + +#: conf.py:20 +msgid "week day" +msgstr "ugedag" + +#: conf.py:21 +msgid "hour" +msgstr "time" + +#: conf.py:22 +msgid "minute" +msgstr "minut" + +#: conf.py:23 +msgid "second" +msgstr "sekund" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "indeholder" + +#: conf.py:29 +msgid "is in" +msgstr "er i" + +#: conf.py:30 +msgid "is greater than" +msgstr "er større end" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "er større end eller lig med" + +#: conf.py:32 +msgid "is less than" +msgstr "er mindre end" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "er mindre end eller lig med" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "starter med" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "slutter med" + +#: conf.py:38 +msgid "is in range" +msgstr "er i intervallet" + +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "matcher regex" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "søg" + +#: conf.py:44 +msgid "is contained by" +msgstr "er indeholdt af" + +#: conf.py:45 +msgid "overlaps" +msgstr "overlapper" + +#: conf.py:46 +msgid "has key" +msgstr "har string" + +#: conf.py:47 +msgid "has keys" +msgstr "har stringe" + +#: conf.py:48 +msgid "has any keys" +msgstr "har hvilken som helst string" + +#: 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:437 +msgid "Today" +msgstr "I dag" + +#: filters.py:438 +msgid "Yesterday" +msgstr "I går" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Sidste 7 dage" + +#: filters.py:440 +msgid "This month" +msgstr "Denne måned" + +#: filters.py:441 +msgid "This year" +msgstr "Dette år" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Flere værdier kan adskilles via komma." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (aftagende)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Sortering" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +#, fuzzy +msgid "Submit" +msgstr "Indsend" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +#, fuzzy +msgid "Field filters" +msgstr "Felt filtre" + +#: utils.py:308 +msgid "exclude" +msgstr "udelad" + +#: widgets.py:58 +msgid "All" +msgstr "Alle" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Ukendt" + +#: widgets.py:162 +msgid "Yes" +msgstr "Ja" + +#: 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 new file mode 100644 index 0000000..9a48a0a Binary files /dev/null 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 new file mode 100644 index 0000000..8a8c48f --- /dev/null +++ b/django_filters/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,193 @@ +# 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: django-filter\n" +"Report-Msgid-Bugs-To: \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" +"Language: de\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: Poedit 1.5.4\n" + +#: conf.py:16 +msgid "date" +msgstr "Datum" + +#: conf.py:17 +msgid "year" +msgstr "Jahr" + +#: conf.py:18 +msgid "month" +msgstr "Monat" + +#: conf.py:19 +msgid "day" +msgstr "Tag" + +#: conf.py:20 +msgid "week day" +msgstr "Wochentag" + +#: conf.py:21 +msgid "hour" +msgstr "Stunde" + +#: conf.py:22 +msgid "minute" +msgstr "Minute" + +#: conf.py:23 +msgid "second" +msgstr "Sekunde" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "enthält" + +#: conf.py:29 +msgid "is in" +msgstr "ist in" + +#: conf.py:30 +msgid "is greater than" +msgstr "ist größer als" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "ist größer oder gleich" + +#: conf.py:32 +msgid "is less than" +msgstr "ist kleiner als" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "ist kleiner oder gleich" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "beginnt mit" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "endet mit" + +#: conf.py:38 +msgid "is in range" +msgstr "ist im Bereich" + +#: conf.py:39 +msgid "is null" +msgstr "" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "passt auf Regex" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "Suche" + +#: conf.py:44 +msgid "is contained by" +msgstr "ist enthalten in" + +#: conf.py:45 +msgid "overlaps" +msgstr "überlappen" + +#: conf.py:46 +msgid "has key" +msgstr "hat Schlüssel" + +#: conf.py:47 +msgid "has keys" +msgstr "hat Schlüssel" + +#: conf.py:48 +msgid "has any keys" +msgstr "hat beliebige Schlüssel" + +#: 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:437 +msgid "Today" +msgstr "Heute" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Gestern" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Letzte 7 Tage" + +#: filters.py:440 +msgid "This month" +msgstr "Diesen Monat" + +#: filters.py:441 +msgid "This year" +msgstr "Dieses Jahr" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Mehrere Werte können durch Kommas getrennt sein." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (absteigend)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Sortierung" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Absenden" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Feldfilter" + +#: utils.py:308 +msgid "exclude" +msgstr "ausschließen" + +#: widgets.py:58 +msgid "All" +msgstr "Alle" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Unbekannte" + +#: widgets.py:162 +msgid "Yes" +msgstr "Ja" + +#: 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 new file mode 100644 index 0000000..4e2258b Binary files /dev/null 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 new file mode 100644 index 0000000..54a2353 --- /dev/null +++ b/django_filters/locale/el/LC_MESSAGES/django.po @@ -0,0 +1,193 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Serafeim Papastefanos , 2017. +# +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-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" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.6.5\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 "περιέχει regex" + +#: 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 "Τις προηγούμενες 7 ημέρες" + +#: 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 "Όχι" + +#~ 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 0000000..3338ff7 Binary files /dev/null and b/django_filters/locale/es/LC_MESSAGES/django.mo differ diff --git a/django_filters/locale/es/LC_MESSAGES/django.po b/django_filters/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..ef5b2ed --- /dev/null +++ b/django_filters/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,195 @@ +# Django Filter translation. +# 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: 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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.16-dev\n" + +#: conf.py:16 +msgid "date" +msgstr "fecha" + +#: conf.py:17 +msgid "year" +msgstr "año" + +#: conf.py:18 +msgid "month" +msgstr "mes" + +#: conf.py:19 +msgid "day" +msgstr "día" + +#: conf.py:20 +msgid "week day" +msgstr "día de la semana" + +#: conf.py:21 +msgid "hour" +msgstr "hora" + +#: conf.py:22 +msgid "minute" +msgstr "minuto" + +#: conf.py:23 +msgid "second" +msgstr "segundo" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "contiene" + +#: conf.py:29 +msgid "is in" +msgstr "presente en" + +#: conf.py:30 +msgid "is greater than" +msgstr "mayor que" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "mayor o igual que" + +#: conf.py:32 +msgid "is less than" +msgstr "menor que" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "menor o igual que" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "comienza por" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "termina por" + +#: conf.py:38 +msgid "is in range" +msgstr "en el rango" + +#: 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:42 conf.py:49 +msgid "search" +msgstr "buscar" + +#: conf.py:44 +msgid "is contained by" +msgstr "contenido en" + +#: conf.py:45 +msgid "overlaps" +msgstr "solapado" + +#: conf.py:46 +msgid "has key" +msgstr "contiene la clave" + +#: conf.py:47 +msgid "has keys" +msgstr "contiene las claves" + +#: conf.py:48 +msgid "has any keys" +msgstr "contiene alguna de las claves" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "Seleccione un operador de consulta." + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "Consultar un rango requiere dos valores." + +#: filters.py:437 +msgid "Today" +msgstr "Hoy" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Ayer" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Últimos 7 días" + +#: filters.py:440 +msgid "This month" +msgstr "Este mes" + +#: filters.py:441 +msgid "This year" +msgstr "Este año" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Múltiples valores separados por comas." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (descendente)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Ordenado" + +#: 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:58 +msgid "All" +msgstr "Todo" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Desconocido" + +#: widgets.py:162 +msgid "Yes" +msgstr "Sí" + +#: widgets.py:162 +msgid "No" +msgstr "No" + +#~ 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 new file mode 100644 index 0000000..7f4778a Binary files /dev/null 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 new file mode 100644 index 0000000..f40b2af --- /dev/null +++ b/django_filters/locale/es_AR/LC_MESSAGES/django.po @@ -0,0 +1,201 @@ +# Django Filter translation. +# Copyright (C) 2013 +# This file is distributed under the same license as the django_filter package. +# Gonzalo Bustos, 2015. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \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" +"Language: es_AR\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: Poedit 1.6.10\n" + +#: conf.py:16 +#, fuzzy +#| msgid "Any date" +msgid "date" +msgstr "Cualquier fecha" + +#: 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:438 +msgid "Yesterday" +msgstr "" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Últimos 7 días" + +#: filters.py:440 +msgid "This month" +msgstr "Este mes" + +#: filters.py:441 +msgid "This year" +msgstr "Este año" + +#: 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 0000000..18cb472 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 0000000..4092d30 --- /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 0000000..9309fe4 --- /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 new file mode 100644 index 0000000..2ae9948 Binary files /dev/null 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 new file mode 100644 index 0000000..2d4da24 --- /dev/null +++ b/django_filters/locale/fr/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. +# Axel Haustant , 2013. +# +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: 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" +"X-Generator: Weblate 5.4-dev\n" + +#: conf.py:16 +msgid "date" +msgstr "date" + +#: conf.py:17 +msgid "year" +msgstr "année" + +#: 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:438 +msgid "Yesterday" +msgstr "Hier" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "7 derniers jours" + +#: filters.py:440 +msgid "This month" +msgstr "Ce mois-ci" + +#: filters.py:441 +msgid "This year" +msgstr "Cette année" + +#: 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/it/LC_MESSAGES/django.mo b/django_filters/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000..f53c4ea Binary files /dev/null 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 0000000..7f8da30 --- /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 0000000..dcfae01 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 0000000..e640c19 --- /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 new file mode 100644 index 0000000..fdbf09d Binary files /dev/null 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 new file mode 100644 index 0000000..3417fe8 --- /dev/null +++ b/django_filters/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,199 @@ +# 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: django_filters 0.0.1\n" +"Report-Msgid-Bugs-To: \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=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:16 +msgid "date" +msgstr "data" + +#: conf.py:17 +msgid "year" +msgstr "rok" + +#: conf.py:18 +msgid "month" +msgstr "miesiąc" + +#: conf.py:19 +msgid "day" +msgstr "dzień" + +#: conf.py:20 +msgid "week day" +msgstr "dzień tygodnia" + +#: conf.py:21 +msgid "hour" +msgstr "godzina" + +#: conf.py:22 +msgid "minute" +msgstr "minuta" + +#: conf.py:23 +msgid "second" +msgstr "sekunda" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "zawiera" + +#: conf.py:29 +msgid "is in" +msgstr "zawiera się w" + +#: conf.py:30 +msgid "is greater than" +msgstr "powyżej" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "powyżej lub równe" + +#: conf.py:32 +msgid "is less than" +msgstr "poniżej" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "poniżej lub równe" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "zaczyna się od" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "kończy się na" + +#: conf.py:38 +msgid "is in range" +msgstr "zawiera się w zakresie" + +#: 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:42 conf.py:49 +msgid "search" +msgstr "szukaj" + +#: conf.py:44 +msgid "is contained by" +msgstr "zawiera się w" + +#: conf.py:45 +msgid "overlaps" +msgstr "nakłada się" + +#: conf.py:46 +msgid "has key" +msgstr "posiada klucz" + +#: conf.py:47 +msgid "has keys" +msgstr "posiada klucze" + +#: conf.py:48 +msgid "has any keys" +msgstr "posiada jakiekolwiek klucze" + +#: fields.py:94 +msgid "Select a lookup." +msgstr "Wybierz wyszukiwanie." + +#: fields.py:198 +msgid "Range query expects two values." +msgstr "Zapytanie o zakres oczekuje dwóch wartości." + +#: filters.py:437 +msgid "Today" +msgstr "Dziś" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Wczoraj" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Ostatnie 7 dni" + +#: filters.py:440 +msgid "This month" +msgstr "Ten miesiąc" + +#: filters.py:441 +msgid "This year" +msgstr "Ten rok" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Wiele wartości można rozdzielić przecinkami." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (malejąco)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Sortowanie" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Wyślij" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Filtry pola" + +#: utils.py:308 +msgid "exclude" +msgstr "wyklucz" + +#: widgets.py:58 +msgid "All" +msgstr "Wszystko" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Nieznane" + +#: widgets.py:162 +msgid "Yes" +msgstr "Tak" + +#: 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 new file mode 100644 index 0000000..e9cc1a8 Binary files /dev/null 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 new file mode 100644 index 0000000..73fca97 --- /dev/null +++ b/django_filters/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,194 @@ +# Django Filter translation. +# Copyright (C) 2017 +# This file is distributed under the same license as the django_filter package. +# Anderson Scouto da Silva, 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-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" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.0-dev\n" + +#: conf.py:16 +msgid "date" +msgstr "data" + +#: conf.py:17 +msgid "year" +msgstr "ano" + +#: conf.py:18 +msgid "month" +msgstr "mês" + +#: conf.py:19 +msgid "day" +msgstr "dia" + +#: conf.py:20 +msgid "week day" +msgstr "dia da semana" + +#: conf.py:21 +msgid "hour" +msgstr "hora" + +#: conf.py:22 +msgid "minute" +msgstr "minuto" + +#: conf.py:23 +msgid "second" +msgstr "segundo" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "contém" + +#: conf.py:29 +msgid "is in" +msgstr "presente em" + +#: conf.py:30 +msgid "is greater than" +msgstr "é maior que" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "é maior ou igual que" + +#: conf.py:32 +msgid "is less than" +msgstr "é menor que" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "é menor ou igual que" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "começa com" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "termina com" + +#: conf.py:38 +msgid "is in range" +msgstr "está no range" + +#: 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:42 conf.py:49 +msgid "search" +msgstr "buscar" + +#: conf.py:44 +msgid "is contained by" +msgstr "está contido por" + +#: conf.py:45 +msgid "overlaps" +msgstr "sobrepõe" + +#: conf.py:46 +msgid "has key" +msgstr "contém a chave" + +#: conf.py:47 +msgid "has keys" +msgstr "contém as chaves" + +#: conf.py:48 +msgid "has any keys" +msgstr "contém uma das chaves" + +#: 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:437 +msgid "Today" +msgstr "Hoje" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Ontem" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Últimos 7 dias" + +#: filters.py:440 +msgid "This month" +msgstr "Este mês" + +#: filters.py:441 +msgid "This year" +msgstr "Este ano" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Valores múltiplos podem ser separados por vírgulas." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (decrescente)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Ordenado" + +#: 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:58 +msgid "All" +msgstr "Tudo" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Desconhecido" + +#: widgets.py:162 +msgid "Yes" +msgstr "Sim" + +#: widgets.py:162 +msgid "No" +msgstr "Não" + +#~ 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 0000000..a622b23 --- /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 new file mode 100644 index 0000000..a4ff1ce Binary files /dev/null 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 new file mode 100644 index 0000000..5a61595 --- /dev/null +++ b/django_filters/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,195 @@ +# 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: django-filter\n" +"Report-Msgid-Bugs-To: \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" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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: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 "Прошедшие 7 дней" + +#: 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 "Нет" + +#~ msgid "Any date" +#~ msgstr "Любая дата" diff --git a/django_filters/locale/sk/LC_MESSAGES/django.mo b/django_filters/locale/sk/LC_MESSAGES/django.mo new file mode 100644 index 0000000..d9c67d8 Binary files /dev/null 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 new file mode 100644 index 0000000..385b3d3 --- /dev/null +++ b/django_filters/locale/sk/LC_MESSAGES/django.po @@ -0,0 +1,196 @@ +# 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 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=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:16 +msgid "date" +msgstr "dátum" + +#: conf.py:17 +msgid "year" +msgstr "rok" + +#: conf.py:18 +msgid "month" +msgstr "mesiac" + +#: conf.py:19 +msgid "day" +msgstr "deň" + +#: conf.py:20 +msgid "week day" +msgstr "deň týždňa" + +#: conf.py:21 +msgid "hour" +msgstr "hodina" + +#: conf.py:22 +msgid "minute" +msgstr "minúta" + +#: conf.py:23 +msgid "second" +msgstr "sekunda" + +#: conf.py:27 conf.py:28 +msgid "contains" +msgstr "obsahuje" + +#: conf.py:29 +msgid "is in" +msgstr "je v" + +#: conf.py:30 +msgid "is greater than" +msgstr "je vačší než" + +#: conf.py:31 +msgid "is greater than or equal to" +msgstr "je vačší alebo rovný ako" + +#: conf.py:32 +msgid "is less than" +msgstr "je menší než" + +#: conf.py:33 +msgid "is less than or equal to" +msgstr "je menší alebo rovný ako" + +#: conf.py:34 conf.py:35 +msgid "starts with" +msgstr "začína s" + +#: conf.py:36 conf.py:37 +msgid "ends with" +msgstr "končí s" + +#: conf.py:38 +msgid "is in range" +msgstr "je v rozsahu" + +#: conf.py:39 +msgid "is null" +msgstr "je nulová" + +#: conf.py:40 conf.py:41 +msgid "matches regex" +msgstr "spĺňa regex" + +#: conf.py:42 conf.py:49 +msgid "search" +msgstr "hľadať" + +#: conf.py:44 +msgid "is contained by" +msgstr "je obsiahnutý" + +#: conf.py:45 +msgid "overlaps" +msgstr "presahuje" + +#: conf.py:46 +msgid "has key" +msgstr "má kľúč" + +#: conf.py:47 +msgid "has keys" +msgstr "má kľúče" + +#: conf.py:48 +msgid "has any keys" +msgstr "má akékoľvek kľúče" + +#: 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:437 +msgid "Today" +msgstr "Dnes" + +#: filters.py:438 +msgid "Yesterday" +msgstr "Včera" + +#: filters.py:439 +msgid "Past 7 days" +msgstr "Posledných 7 dní" + +#: filters.py:440 +msgid "This month" +msgstr "Tento mesiac" + +#: filters.py:441 +msgid "This year" +msgstr "Tento rok" + +#: filters.py:543 +msgid "Multiple values may be separated by commas." +msgstr "Viacero hodnôt môže byť oddelených čiarkami." + +#: filters.py:721 +#, python-format +msgid "%s (descending)" +msgstr "%s (klesajúco)" + +#: filters.py:737 +msgid "Ordering" +msgstr "Zoradenie" + +#: rest_framework/filterset.py:33 +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "Potvrdiť" + +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "Filtre poľa" + +#: utils.py:308 +msgid "exclude" +msgstr "neobsahuje" + +#: widgets.py:58 +msgid "All" +msgstr "Všetky" + +#: widgets.py:162 +msgid "Unknown" +msgstr "Neznáme" + +#: widgets.py:162 +msgid "Yes" +msgstr "Áno" + +#: 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 new file mode 100644 index 0000000..e16148d Binary files /dev/null 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 new file mode 100644 index 0000000..ead6f2d --- /dev/null +++ b/django_filters/locale/uk/LC_MESSAGES/django.po @@ -0,0 +1,193 @@ +# +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: 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=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: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 "Минулі 7 днів" + +#: 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 "Немає" + +#~ 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 0000000..7047a5e 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 new file mode 100644 index 0000000..9fb5ce9 --- /dev/null +++ b/django_filters/locale/zh_CN/LC_MESSAGES/django.po @@ -0,0 +1,194 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Kane Blueriver , 2016. +# +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: 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" + +#: 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 "过去 7 日" + +#: 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 "否" + +#~ msgid "This is an exclusion filter" +#~ msgstr "未启用该过滤器" diff --git a/django_filters/rest_framework/__init__.py b/django_filters/rest_framework/__init__.py new file mode 100644 index 0000000..4ffc408 --- /dev/null +++ b/django_filters/rest_framework/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa +from .backends import DjangoFilterBackend +from .filters import * +from .filterset import FilterSet diff --git a/django_filters/rest_framework/__pycache__/__init__.cpython-312.pyc b/django_filters/rest_framework/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..98d0ec4 Binary files /dev/null and b/django_filters/rest_framework/__pycache__/__init__.cpython-312.pyc differ diff --git a/django_filters/rest_framework/__pycache__/backends.cpython-312.pyc b/django_filters/rest_framework/__pycache__/backends.cpython-312.pyc new file mode 100644 index 0000000..13efcfa Binary files /dev/null and b/django_filters/rest_framework/__pycache__/backends.cpython-312.pyc differ diff --git a/django_filters/rest_framework/__pycache__/filters.cpython-312.pyc b/django_filters/rest_framework/__pycache__/filters.cpython-312.pyc new file mode 100644 index 0000000..c9eb37b Binary files /dev/null and b/django_filters/rest_framework/__pycache__/filters.cpython-312.pyc differ diff --git a/django_filters/rest_framework/__pycache__/filterset.cpython-312.pyc b/django_filters/rest_framework/__pycache__/filterset.cpython-312.pyc new file mode 100644 index 0000000..69e75e6 Binary files /dev/null and b/django_filters/rest_framework/__pycache__/filterset.cpython-312.pyc differ diff --git a/django_filters/rest_framework/backends.py b/django_filters/rest_framework/backends.py new file mode 100644 index 0000000..bb2ae35 --- /dev/null +++ b/django_filters/rest_framework/backends.py @@ -0,0 +1,165 @@ +import warnings + +from django.template import loader + +from .. import compat, utils +from . import filters, filterset + + +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" + + def get_filterset(self, request, queryset, view): + filterset_class = self.get_filterset_class(view, queryset) + if filterset_class is None: + return None + + kwargs = self.get_filterset_kwargs(request, queryset, view) + return filterset_class(**kwargs) + + 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) + + 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, + ) + + return filterset_class + + if filterset_fields and queryset is not None: + MetaBase = getattr(self.filterset_base, "Meta", object) + + class AutoFilterSet(self.filterset_base): + class Meta(MetaBase): + model = queryset.model + fields = filterset_fields + + return AutoFilterSet + + return None + + def get_filterset_kwargs(self, request, queryset, view): + return { + "data": request.query_params, + "queryset": queryset, + "request": request, + } + + def filter_queryset(self, request, queryset, view): + filterset = self.get_filterset(request, queryset, view) + if filterset is None: + return queryset + + if not filterset.is_valid() and self.raise_exception: + raise utils.translate_validation(filterset.errors) + return filterset.qs + + def to_html(self, request, queryset, view): + filterset = self.get_filterset(request, queryset, view) + if filterset is None: + return None + + template = loader.get_template(self.template) + context = {"filter": filterset} + return template.render(context, request) + + def get_coreschema_field(self, field): + if isinstance(field, filters.NumberFilter): + field_cls = compat.coreschema.Number + else: + field_cls = compat.coreschema.String + 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... + 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() + except Exception: + queryset = None + warnings.warn( + "{} is not compatible with schema generation".format(view.__class__) + ) + + filterset_class = self.get_filterset_class(view, queryset) + + return ( + [] + if not filterset_class + else [ + 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: + queryset = None + warnings.warn( + "{} is not compatible with schema generation".format(view.__class__) + ) + + filterset_class = self.get_filterset_class(view, queryset) + + if not filterset_class: + return [] + + parameters = [] + for field_name, field in filterset_class.base_filters.items(): + parameter = { + "name": field_name, + "required": field.extra["required"], + "in": "query", + "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"]] + parameters.append(parameter) + return parameters diff --git a/django_filters/rest_framework/filters.py b/django_filters/rest_framework/filters.py new file mode 100644 index 0000000..e2c8f10 --- /dev/null +++ b/django_filters/rest_framework/filters.py @@ -0,0 +1,13 @@ +from django_filters import filters + +from ..filters import * # noqa +from ..widgets import BooleanWidget + +__all__ = filters.__all__ + + +class BooleanFilter(filters.BooleanFilter): + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", BooleanWidget) + + super().__init__(*args, **kwargs) diff --git a/django_filters/rest_framework/filterset.py b/django_filters/rest_framework/filterset.py new file mode 100644 index 0000000..c27f77b --- /dev/null +++ b/django_filters/rest_framework/filterset.py @@ -0,0 +1,41 @@ +from copy import deepcopy + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from django_filters import filterset + +from .. import compat +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}, + } +) + + +class FilterSet(filterset.FilterSet): + FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS + + @property + def form(self): + form = super().form + + if compat.is_crispy(): + from crispy_forms.helper import FormHelper + from crispy_forms.layout import Layout, Submit + + layout_components = list(form.fields.keys()) + [ + Submit("", _("Submit"), css_class="btn-default"), + ] + helper = FormHelper() + helper.form_method = "GET" + helper.layout = Layout(*layout_components) + + form.helper = helper + + return form diff --git a/django_filters/templates/django_filters/rest_framework/crispy_form.html b/django_filters/templates/django_filters/rest_framework/crispy_form.html new file mode 100644 index 0000000..171767c --- /dev/null +++ b/django_filters/templates/django_filters/rest_framework/crispy_form.html @@ -0,0 +1,5 @@ +{% load crispy_forms_tags %} +{% load i18n %} + +

{% trans "Field filters" %}

+{% crispy filter.form %} diff --git a/django_filters/templates/django_filters/rest_framework/form.html b/django_filters/templates/django_filters/rest_framework/form.html new file mode 100644 index 0000000..b116e35 --- /dev/null +++ b/django_filters/templates/django_filters/rest_framework/form.html @@ -0,0 +1,6 @@ +{% load i18n %} +

{% trans "Field filters" %}

+
+ {{ filter.form.as_p }} + +
diff --git a/django_filters/templates/django_filters/widgets/multiwidget.html b/django_filters/templates/django_filters/widgets/multiwidget.html new file mode 100644 index 0000000..089ddb2 --- /dev/null +++ b/django_filters/templates/django_filters/widgets/multiwidget.html @@ -0,0 +1 @@ +{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %} diff --git a/django_filters/utils.py b/django_filters/utils.py new file mode 100644 index 0000000..9b3f872 --- /dev/null +++ b/django_filters/utils.py @@ -0,0 +1,350 @@ +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 +from django.db.models.constants import LOOKUP_SEP +from django.db.models.expressions import Expression +from django.db.models.fields.related import ForeignObjectRel, RelatedField +from django.utils import timezone +from django.utils.encoding import force_str +from django.utils.text import capfirst +from django.utils.translation import gettext as _ + +from .exceptions import FieldLookupError + + +def deprecate(msg, level_modifier=0): + warnings.warn(msg, MigrationNotice, stacklevel=3 + level_modifier) + + +class MigrationNotice(DeprecationWarning): + url = "https://django-filter.readthedocs.io/en/main/guide/migration.html" + + def __init__(self, message): + super().__init__("%s See: %s" % (message, self.url)) + + +class RenameAttributesBase(type): + """ + Handles the deprecation paths when renaming an attribute. + + It does the following: + - Defines accessors that redirect to the renamed attributes. + - Complain whenever an old attribute is accessed. + + This is conceptually based on `django.utils.deprecation.RenameMethodsBase`. + """ + + renamed_attributes = () + + def __new__(metacls, name, bases, attrs): + # remove old attributes before creating class + old_names = [r[0] for r in metacls.renamed_attributes] + old_names = [name for name in old_names if name in 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) + + new_class = super().__new__(metacls, name, bases, attrs) + + 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__"): + return super(new_class, self).__getattr__(name) + return self.__getattribute__(name) + + def __setattr__(self, name, value): + name = type(self).get_name(name) + if cls_setattr is not None: + return cls_setattr(self, name, value) + return super(new_class, self).__setattr__(name, value) + + new_class.__getattr__ = __getattr__ + new_class.__setattr__ = __setattr__ + + # set renamed attributes + for name, value in old_attrs.items(): + setattr(new_class, name, value) + + return new_class + + def get_name(metacls, name): + """ + Get the real attribute name. If the attribute has been renamed, + the new name will be returned and a deprecation warning issued. + """ + for renamed_attribute in metacls.renamed_attributes: + 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, + ) + return new_name + + return name + + def __getattr__(metacls, name): + return super().__getattribute__(metacls.get_name(name)) + + def __setattr__(metacls, name, value): + return super().__setattr__(metacls.get_name(name), value) + + +def try_dbfield(fn, field_class): + """ + Try ``fn`` with the DB ``field_class`` by walking its + MRO until a result is found. + + ex:: + _try_dbfield(field_dict.get, models.CharField) + + """ + # walk the mro, as field_class could be a derived model field. + for cls in field_class.mro(): + # skip if cls is models.Field + if cls is models.Field: + continue + + data = fn(cls) + if data: + return data + + +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)) + ] + + +def get_model_field(model, field_name): + """ + Get a ``model`` field, traversing relationships + in the ``field_name``. + + ex:: + + f = get_model_field(Book, 'author__first_name') + + """ + fields = get_field_parts(model, field_name) + return fields[-1] if fields else None + + +def get_field_parts(model, field_name): + """ + Get the field parts that represent the traversable relationships from the + base ``model`` to the final field, described by ``field_name``. + + ex:: + + >>> parts = get_field_parts(Book, 'author__first_name') + >>> [p.verbose_name for p in parts] + ['author', 'first name'] + + """ + parts = field_name.split(LOOKUP_SEP) + opts = model._meta + fields = [] + + # walk relationships + for name in parts: + try: + field = opts.get_field(name) + except FieldDoesNotExist: + return None + + fields.append(field) + 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 + + +def resolve_field(model_field, lookup_expr): + """ + Resolves a ``lookup_expr`` into its final output field, given + the initial ``model_field``. The lookup expression should only contain + transforms and lookups, not intermediary model field parts. + + Note: + This method is based on django.db.models.sql.query.Query.build_lookup + + For more info on the lookup API: + https://docs.djangoproject.com/en/stable/ref/models/lookups/ + + """ + query = model_field.model._default_manager.all().query + lhs = Expression(model_field) + lookups = lookup_expr.split(LOOKUP_SEP) + + assert len(lookups) > 0 + + try: + while lookups: + name = lookups[0] + args = (lhs, name) + # If there is just one part left, try first get_lookup() so + # that if the lhs supports both transform and lookup for the + # name, then lookup will be picked. + if len(lookups) == 1: + final_lookup = lhs.get_lookup(name) + if not final_lookup: + # We didn't find a lookup. We are going to interpret + # the name as transform, and do an Exact lookup against + # it. + lhs = query.try_transform(*args) + final_lookup = lhs.get_lookup("exact") + return lhs.output_field, final_lookup.lookup_name + lhs = query.try_transform(*args) + lookups = lookups[1:] + except FieldError as e: + raise FieldLookupError(model_field, lookup_expr) from e + + +def handle_timezone(value, is_dst=None): + if settings.USE_TZ and timezone.is_naive(value): + # On pre-5.x 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 4.2 is no longer supported upstream. + if 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, datetime.timezone.utc) + return value + + +def verbose_field_name(model, field_name): + """ + Get the verbose name for a given ``field_name``. The ``field_name`` + will be traversed across relationships. Returns '[invalid name]' for + any field name that cannot be traversed. + + ex:: + + >>> verbose_field_name(Article, 'author__name') + 'author name' + + """ + if field_name is None: + return "[invalid name]" + + parts = get_field_parts(model, field_name) + if not parts: + return "[invalid name]" + + names = [] + for part in parts: + if isinstance(part, ForeignObjectRel): + if part.related_name: + names.append(part.related_name.replace("_", " ")) + else: + return "[invalid name]" + else: + names.append(force_str(part.verbose_name)) + + return " ".join(names) + + +def verbose_lookup_expr(lookup_expr): + """ + Get a verbose, more humanized expression for a given ``lookup_expr``. + Each part in the expression is looked up in the ``FILTERS_VERBOSE_LOOKUPS`` + dictionary. Missing keys will simply default to itself. + + ex:: + + >>> verbose_lookup_expr('year__lt') + 'year is less than' + + # with `FILTERS_VERBOSE_LOOKUPS = {}` + >>> verbose_lookup_expr('year__lt') + 'year lt' + + """ + from .conf import settings as app_settings + + VERBOSE_LOOKUPS = app_settings.VERBOSE_LOOKUPS or {} + lookups = [ + force_str(VERBOSE_LOOKUPS.get(lookup, _(lookup))) + for lookup in lookup_expr.split(LOOKUP_SEP) + ] + + return " ".join(lookups) + + +def label_for_filter(model, field_name, lookup_expr, exclude=False): + """ + Create a generic label suitable for a filter. + + ex:: + + >>> label_for_filter(Article, 'author__name', 'in') + 'auther name is in' + + """ + name = verbose_field_name(model, field_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)) + + return verbose_expression + + +def translate_validation(error_dict): + """ + Translate a Django ErrorDict into its DRF ValidationError. + """ + # it's necessary to lazily import the exception, as it can otherwise create + # an import loop when importing django_filters inside the project settings. + from rest_framework.exceptions import ErrorDetail, ValidationError + + exc = OrderedDict( + ( + 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() + ) + + return ValidationError(exc) diff --git a/django_filters/views.py b/django_filters/views.py new file mode 100644 index 0000000..c24f9ab --- /dev/null +++ b/django_filters/views.py @@ -0,0 +1,129 @@ +from django.core.exceptions import ImproperlyConfigured +from django.views.generic import View +from django.views.generic.list import ( + MultipleObjectMixin, + MultipleObjectTemplateResponseMixin, +) + +from .constants import ALL_FIELDS +from .filterset import filterset_factory + + +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 + + def get_filterset_class(self): + """ + Returns the filterset class to use in this view + """ + if self.filterset_class: + return self.filterset_class + elif self.model: + return filterset_factory(model=self.model, fields=self.filterset_fields) + else: + msg = "'%s' must define 'filterset_class' or 'model'" + raise ImproperlyConfigured(msg % self.__class__.__name__) + + def get_filterset(self, filterset_class): + """ + Returns an instance of the filterset to be used in this view. + """ + kwargs = self.get_filterset_kwargs(filterset_class) + return filterset_class(**kwargs) + + 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, + } + try: + 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." + ) + args = (filterset_class.__name__, self.__class__.__name__) + raise ImproperlyConfigured(msg % args) + return kwargs + + def get_strict(self): + return self.strict + + +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() + ): + 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 + ) + return self.render_to_response(context) + + +class FilterView(MultipleObjectTemplateResponseMixin, BaseFilterView): + """ + Render some list of objects with filter, set by `self.model` or + `self.queryset`. + `self.queryset` can actually be any iterable of items, not just a queryset. + """ + + template_name_suffix = "_filter" + + +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 {} + 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, + ) + view = ECFilterView.as_view(**kwargs) + return view(request, extra_context=extra_context) diff --git a/django_filters/widgets.py b/django_filters/widgets.py new file mode 100644 index 0000000..f5c99b1 --- /dev/null +++ b/django_filters/widgets.py @@ -0,0 +1,270 @@ +from collections.abc import Iterable +from copy import deepcopy +from itertools import chain +from re import search, sub + +from django import forms +from django.db.models.fields import BLANK_CHOICE_DASH +from django.forms.utils import flatatt +from django.utils.datastructures import MultiValueDict +from django.utils.encoding import force_str +from django.utils.http import urlencode +from django.utils.safestring import mark_safe +from django.utils.translation import gettext as _ + + +class LinkWidget(forms.Widget): + def __init__(self, attrs=None, choices=()): + super().__init__(attrs) + + self.choices = choices + + def value_from_datadict(self, data, files, name): + value = super().value_from_datadict(data, files, name) + self.data = data + return value + + def render(self, name, value, attrs=None, choices=(), renderer=None): + if not hasattr(self, "data"): + self.data = {} + if value is None: + value = "" + final_attrs = self.build_attrs(self.attrs, extra_attrs=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)) + + def render_options(self, choices, selected_choices, name): + selected_choices = set(force_str(v) for v in selected_choices) + output = [] + for option_value, option_label in chain(self.choices, choices): + if isinstance(option_label, (list, tuple)): + for option in option_label: + 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) + + 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") + data = self.data.copy() + data[name] = option_value + selected = data == self.data or option_value in selected_choices + try: + url = data.urlencode() + except AttributeError: + url = urlencode(data) + return self.option_string() % { + "attrs": selected and ' class="selected"' or "", + "query_string": url, + "label": force_str(option_label), + } + + def option_string(self): + return '
  • %(label)s
  • ' + + +class SuffixedMultiWidget(forms.MultiWidget): + """ + A MultiWidget that allows users to provide custom suffixes instead of indexes. + + - Suffixes must be unique. + - There must be the same number of suffixes as fields. + """ + + suffixes = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + assert len(self.widgets) == len(self.suffixes) + assert len(self.suffixes) == len(set(self.suffixes)) + + def suffixed(self, name, suffix): + 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) + + return context + + def value_from_datadict(self, data, files, name): + return [ + widget.value_from_datadict(data, files, self.suffixed(name, suffix)) + for widget, suffix in zip(self.widgets, self.suffixes) + ] + + def value_omitted_from_data(self, data, files, name): + return all( + widget.value_omitted_from_data(data, files, self.suffixed(name, suffix)) + for widget, suffix in zip(self.widgets, self.suffixes) + ) + + def replace_name(self, output, index): + result = search(r'name="(?P.*)_%d"' % index, output) + name = result.group("name") + name = self.suffixed(name, self.suffixes[index]) + name = 'name="%s"' % name + + return sub(r'name=".*_%d"' % index, name, output) + + def decompress(self, value): + if value is None: + return [None, None] + return value + + +class RangeWidget(SuffixedMultiWidget): + template_name = "django_filters/widgets/multiwidget.html" + suffixes = ["min", "max"] + + def __init__(self, attrs=None): + widgets = (forms.TextInput, forms.TextInput) + super().__init__(widgets, attrs) + + def decompress(self, value): + if value: + return [value.start, value.stop] + return [None, None] + + +class DateRangeWidget(RangeWidget): + suffixes = ["after", "before"] + + +class LookupChoiceWidget(SuffixedMultiWidget): + suffixes = [None, "lookup"] + + def decompress(self, value): + if value is None: + return [None, None] + return value + + +class BooleanWidget(forms.Select): + """Convert true/false values into the internal Python True/False. + 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"))) + super().__init__(attrs, choices) + + def render(self, name, value, attrs=None, renderer=None): + try: + value = {True: "true", False: "false", "1": "true", "0": "false"}[value] + except KeyError: + value = "" + return super().render(name, value, attrs, renderer=renderer) + + def value_from_datadict(self, data, files, name): + value = data.get(name, None) + if isinstance(value, str): + value = value.lower() + + return { + "1": True, + "0": False, + "true": True, + "false": False, + True: True, + False: False, + }.get(value, None) + + +class BaseCSVWidget(forms.Widget): + # Surrogate widget for rendering multiple values + surrogate = forms.TextInput + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if isinstance(self.surrogate, type): + self.surrogate = self.surrogate() + else: + self.surrogate = deepcopy(self.surrogate) + + def _isiterable(self, value): + return isinstance(value, Iterable) and not isinstance(value, str) + + 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 + 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 None + + def render(self, name, value, attrs=None, renderer=None): + if not self._isiterable(value): + value = [value] + + if len(value) <= 1: + # delegate to main widget (Select, etc...) if not multiple values + 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)) + + return self.surrogate.render(name, value, attrs, renderer=renderer) + + +class CSVWidget(BaseCSVWidget, forms.TextInput): + def __init__(self, *args, attrs=None, **kwargs): + super().__init__(*args, attrs, **kwargs) + + if attrs is not None: + self.surrogate.attrs.update(attrs) + + +class QueryArrayWidget(BaseCSVWidget, forms.TextInput): + """ + Enables request query array notation that might be consumed by MultipleChoiceFilter + + 1. Values can be provided as csv string: ?foo=bar,baz + 2. Values can be provided as query array: ?foo[]=bar&foo[]=baz + 3. Values can be provided as query array: ?foo=bar&foo=baz + + Note: Duplicate and empty values are skipped from results + """ + + 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 = MultiValueDict(data) + + 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] + # ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2] + if len(values_list) > 0: + ret = [x for x in values_list if x] + else: + ret = [] + + return list(set(ret))