commit
ee7dfa6316
@ -0,0 +1 @@
|
||||
pip
|
@ -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.
|
@ -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 <alex.gaynor@gmail.com>
|
||||
Maintainer-email: Carlton Gibson <carlton.gibson@noumenal.es>
|
||||
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 <https://devguide.python.org/versions/#supported-versions>`_
|
||||
* `List of supported Django versions <https://www.djangoproject.com/download/#supported-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 <https://noumenal.es/>`_.
|
||||
|
||||
.. _`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
|
||||
|
@ -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
|
@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: flit 3.8.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
@ -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)
|
@ -0,0 +1,4 @@
|
||||
ALL_FIELDS = "__all__"
|
||||
|
||||
|
||||
EMPTY_VALUES = ([], (), {}, "", None)
|
@ -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)
|
||||
)
|
@ -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
|
@ -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
|
||||
<Type>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
|
@ -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
|
||||
<Type>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}
|
||||
)
|
Binary file not shown.
@ -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 <EMAIL@ADDRESS>, YEAR.
|
||||
# FULL NAME <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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 "لا"
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 <eugena.mihailikova@gmail.com>\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"
|
Binary file not shown.
@ -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 <danni@danniranderis.dk>\n"
|
||||
"Language-Team: Danni Randeris <danni@danniranderis.dk>\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"
|
Binary file not shown.
@ -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 <EMAIL@ADDRESS>, 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 <florian@apolloner.eu>\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"
|
Binary file not shown.
Binary file not shown.
@ -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 <fran-carro@hotmail.es>\n"
|
||||
"Language-Team: Spanish <https://hosted.weblate.org/projects/django-filter/"
|
||||
"django-filter/es/>\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"
|
Binary file not shown.
@ -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"
|
Binary file not shown.
@ -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 <janne.tervo@iki.fi>\n"
|
||||
"Language-Team: Finnish <https://hosted.weblate.org/projects/django-filter/"
|
||||
"django-filter/fi/>\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"
|
Binary file not shown.
@ -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 <noirbizarre@gmail.com>, 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 <nils.van-zuijlen@mailo.com>\n"
|
||||
"Language-Team: French <https://hosted.weblate.org/projects/django-filter/"
|
||||
"django-filter/fr/>\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"
|
Binary file not shown.
@ -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 <eriol@mornie.org>\n"
|
||||
"Language-Team: Italian <https://hosted.weblate.org/projects/django-filter/"
|
||||
"django-filter/it/>\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"
|
Binary file not shown.
@ -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 <EMAIL@ADDRESS>, 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 <storm@stormbase.digital>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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"
|
Binary file not shown.
Binary file not shown.
@ -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 <diogosilv30@gmail.com>\n"
|
||||
"Language-Team: Portuguese (Brazil) <https://hosted.weblate.org/projects/"
|
||||
"django-filter/django-filter/pt_BR/>\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"
|
@ -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 <EMAIL@ADDRESS>, 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 <dan@zerolab.org>\n"
|
||||
"Language-Team: Romanian <https://hosted.weblate.org/projects/django-filter/"
|
||||
"django-filter/ro/>\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"
|
Binary file not shown.
Binary file not shown.
@ -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 <EMAIL@ADDRESS>, 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 <salka.milan@googlemail.com>\n"
|
||||
"Language-Team: Slovak <https://hosted.weblate.org/projects/django-filter/"
|
||||
"django-filter/sk/>\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"
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,4 @@
|
||||
# flake8: noqa
|
||||
from .backends import DjangoFilterBackend
|
||||
from .filters import *
|
||||
from .filterset import FilterSet
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 `<name>_0`, `<name>_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
|
@ -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)
|
@ -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
|
@ -0,0 +1,5 @@
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
{% crispy filter.form %}
|
@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
<form class="form" action="" method="get">
|
||||
{{ filter.form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
||||
</form>
|
@ -0,0 +1 @@
|
||||
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}
|
@ -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)
|
@ -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)
|
@ -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 = ["<ul%s>" % flatatt(final_attrs)]
|
||||
options = self.render_options(choices, [value], name)
|
||||
if options:
|
||||
output.append(options)
|
||||
output.append("</ul>")
|
||||
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 '<li><a%(attrs)s href="?%(query_string)s">%(label)s</a></li>'
|
||||
|
||||
|
||||
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<name>.*)_%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))
|
Loading…
Reference in new issue