django-filter-23.5/0000755000175000017500000000000014533556010014052 5ustar carstencarstendjango-filter-23.5/django_filters/0000755000175000017500000000000014533556010017044 5ustar carstencarstendjango-filter-23.5/django_filters/locale/0000755000175000017500000000000014533556010020303 5ustar carstencarstendjango-filter-23.5/django_filters/locale/pt_BR/0000755000175000017500000000000014533556010021311 5ustar carstencarstendjango-filter-23.5/django_filters/locale/pt_BR/LC_MESSAGES/0000755000175000017500000000000014533556010023076 5ustar carstencarstendjango-filter-23.5/django_filters/locale/pt_BR/LC_MESSAGES/django.po0000644000175000017500000000655214533556010024710 0ustar carstencarsten# Django Filter translation. # Copyright (C) 2017 # This file is distributed under the same license as the django_filter package. # Anderson Scouto da Silva, 2017. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2023-06-30 13:51+0000\n" "Last-Translator: Diogo Silva \n" "Language-Team: Portuguese (Brazil) \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 5.0-dev\n" #: conf.py:16 msgid "date" msgstr "data" #: conf.py:17 msgid "year" msgstr "ano" #: conf.py:18 msgid "month" msgstr "mês" #: conf.py:19 msgid "day" msgstr "dia" #: conf.py:20 msgid "week day" msgstr "dia da semana" #: conf.py:21 msgid "hour" msgstr "hora" #: conf.py:22 msgid "minute" msgstr "minuto" #: conf.py:23 msgid "second" msgstr "segundo" #: conf.py:27 conf.py:28 msgid "contains" msgstr "contém" #: conf.py:29 msgid "is in" msgstr "presente em" #: conf.py:30 msgid "is greater than" msgstr "é maior que" #: conf.py:31 msgid "is greater than or equal to" msgstr "é maior ou igual que" #: conf.py:32 msgid "is less than" msgstr "é menor que" #: conf.py:33 msgid "is less than or equal to" msgstr "é menor ou igual que" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "começa com" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "termina com" #: conf.py:38 msgid "is in range" msgstr "está no range" #: conf.py:39 msgid "is null" msgstr "é nulo" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "coincide com a expressão regular" #: conf.py:42 conf.py:49 msgid "search" msgstr "buscar" #: conf.py:44 msgid "is contained by" msgstr "está contido por" #: conf.py:45 msgid "overlaps" msgstr "sobrepõe" #: conf.py:46 msgid "has key" msgstr "contém a chave" #: conf.py:47 msgid "has keys" msgstr "contém as chaves" #: conf.py:48 msgid "has any keys" msgstr "contém uma das chaves" #: fields.py:94 msgid "Select a lookup." msgstr "Selecione uma pesquisa." #: fields.py:198 msgid "Range query expects two values." msgstr "Consulta por range requer dois valores." #: filters.py:437 msgid "Today" msgstr "Hoje" #: filters.py:438 msgid "Yesterday" msgstr "Ontem" #: filters.py:439 msgid "Past 7 days" msgstr "Últimos 7 dias" #: filters.py:440 msgid "This month" msgstr "Este mês" #: filters.py:441 msgid "This year" msgstr "Este ano" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Valores múltiplos podem ser separados por vírgulas." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (decrescente)" #: filters.py:737 msgid "Ordering" msgstr "Ordenado" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Enviar" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Filtros de campo" #: utils.py:308 msgid "exclude" msgstr "excluir" #: widgets.py:58 msgid "All" msgstr "Tudo" #: widgets.py:162 msgid "Unknown" msgstr "Desconhecido" #: widgets.py:162 msgid "Yes" msgstr "Sim" #: widgets.py:162 msgid "No" msgstr "Não" #~ msgid "Any date" #~ msgstr "Qualquer data" django-filter-23.5/django_filters/locale/uk/0000755000175000017500000000000014533556010020722 5ustar carstencarstendjango-filter-23.5/django_filters/locale/uk/LC_MESSAGES/0000755000175000017500000000000014533556010022507 5ustar carstencarstendjango-filter-23.5/django_filters/locale/uk/LC_MESSAGES/django.po0000644000175000017500000000732014533556010024313 0ustar carstencarsten# msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2016-09-29 11:47+0300\n" "Last-Translator: Eugena Mikhaylikova \n" "Language-Team: TextTempearture\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != " "11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % " "100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || " "(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" "X-Generator: Poedit 1.8.9\n" #: conf.py:16 msgid "date" msgstr "дата" #: conf.py:17 msgid "year" msgstr "рік" #: conf.py:18 msgid "month" msgstr "місяць" #: conf.py:19 msgid "day" msgstr "день" #: conf.py:20 msgid "week day" msgstr "день тижня" #: conf.py:21 msgid "hour" msgstr "година" #: conf.py:22 msgid "minute" msgstr "хвилина" #: conf.py:23 msgid "second" msgstr "секунда" #: conf.py:27 conf.py:28 msgid "contains" msgstr "містить" #: conf.py:29 msgid "is in" msgstr "в" #: conf.py:30 msgid "is greater than" msgstr "більше ніж" #: conf.py:31 msgid "is greater than or equal to" msgstr "більше або дорівнює" #: conf.py:32 msgid "is less than" msgstr "менше ніж" #: conf.py:33 msgid "is less than or equal to" msgstr "менше або дорівнює" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "починається" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "закінчується" #: conf.py:38 msgid "is in range" msgstr "в діапазоні" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "відповідає регулярному виразу" #: conf.py:42 conf.py:49 msgid "search" msgstr "пошук" #: conf.py:44 msgid "is contained by" msgstr "міститься в" #: conf.py:45 msgid "overlaps" msgstr "перекривається" #: conf.py:46 msgid "has key" msgstr "має ключ" #: conf.py:47 msgid "has keys" msgstr "має ключі" #: conf.py:48 msgid "has any keys" msgstr "має будь-які ключі" #: fields.py:94 msgid "Select a lookup." msgstr "" #: fields.py:198 msgid "Range query expects two values." msgstr "Запит діапазону очікує два значення." #: filters.py:437 msgid "Today" msgstr "Сьогодні" #: filters.py:438 msgid "Yesterday" msgstr "Вчора" #: filters.py:439 msgid "Past 7 days" msgstr "Минулі 7 днів" #: filters.py:440 msgid "This month" msgstr "За цей місяць" #: filters.py:441 msgid "This year" msgstr "В цьому році" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Кілька значень можуть бути розділені комами." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (по спадаючій)" #: filters.py:737 msgid "Ordering" msgstr "Порядок" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Відправити" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Фільтри по полях" #: utils.py:308 msgid "exclude" msgstr "виключаючи" #: widgets.py:58 msgid "All" msgstr "Усе" #: widgets.py:162 msgid "Unknown" msgstr "Не задано" #: widgets.py:162 msgid "Yes" msgstr "Так" #: widgets.py:162 msgid "No" msgstr "Немає" #~ msgid "Any date" #~ msgstr "Будь-яка дата" django-filter-23.5/django_filters/locale/fi/0000755000175000017500000000000014533556010020701 5ustar carstencarstendjango-filter-23.5/django_filters/locale/fi/LC_MESSAGES/0000755000175000017500000000000014533556010022466 5ustar carstencarstendjango-filter-23.5/django_filters/locale/fi/LC_MESSAGES/django.po0000644000175000017500000000655114533556010024277 0ustar carstencarsten# Django Filter translation. # Copyright (C) 2013 # This file is distributed under the same license as the django_filter package. # Carlos Goce, 2017. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 17:45+0200\n" "PO-Revision-Date: 2023-02-12 14:36+0000\n" "Last-Translator: Janne Tervo \n" "Language-Team: Finnish \n" "Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.16-dev\n" #: conf.py:16 msgid "date" msgstr "päivämäärä" #: conf.py:17 msgid "year" msgstr "vuosi" #: conf.py:18 msgid "month" msgstr "kuukausi" #: conf.py:19 msgid "day" msgstr "päivä" #: conf.py:20 msgid "week day" msgstr "viikonpäivä" #: conf.py:21 msgid "hour" msgstr "tunti" #: conf.py:22 msgid "minute" msgstr "minuutti" #: conf.py:23 msgid "second" msgstr "sekunti" #: conf.py:27 conf.py:28 msgid "contains" msgstr "sisältää" #: conf.py:29 msgid "is in" msgstr "löytyy" #: conf.py:30 msgid "is greater than" msgstr "suurempi kuin" #: conf.py:31 msgid "is greater than or equal to" msgstr "suurempi tai yhtäsuuri kuin" #: conf.py:32 msgid "is less than" msgstr "pienempi kuin" #: conf.py:33 msgid "is less than or equal to" msgstr "pienempi tai yhtäsuuri kuin" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "alkaa" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "päättyy" #: conf.py:38 msgid "is in range" msgstr "on välillä" #: conf.py:39 msgid "is null" msgstr "on null" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "täsmää säännölliseen lausekkeeseen" #: conf.py:42 conf.py:49 msgid "search" msgstr "hae" #: conf.py:44 msgid "is contained by" msgstr "sisältyy kokonaan" #: conf.py:45 msgid "overlaps" msgstr "on päällekkäinen" #: conf.py:46 msgid "has key" msgstr "sisältää avaimen" #: conf.py:47 msgid "has keys" msgstr "sisältää avaimet" #: conf.py:48 msgid "has any keys" msgstr "sisältää minkä tahansa avaimista" #: fields.py:94 msgid "Select a lookup." msgstr "Hakuehto vaaditaan." #: fields.py:198 msgid "Range query expects two values." msgstr "Välin hakuun tarvitaan kaksi arvoa." #: filters.py:437 msgid "Today" msgstr "Tänään" #: filters.py:438 msgid "Yesterday" msgstr "Eilen" #: filters.py:439 msgid "Past 7 days" msgstr "Edelliset 7 päivää" #: filters.py:440 msgid "This month" msgstr "Tässä kuussa" #: filters.py:441 msgid "This year" msgstr "Tänä vuonna" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Voit syöttää useita arvoja pilkulla erotettuna." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (laskeva)" #: filters.py:737 msgid "Ordering" msgstr "Järjestä" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Lähetä" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Kenttävalinnat" #: utils.py:312 msgid "exclude" msgstr "poissulje" #: widgets.py:58 msgid "All" msgstr "Kaikki" #: widgets.py:162 msgid "Unknown" msgstr "Tuntematon" #: widgets.py:162 msgid "Yes" msgstr "Kyllä" #: widgets.py:162 msgid "No" msgstr "Ei" django-filter-23.5/django_filters/locale/nl/0000755000175000017500000000000014533556010020714 5ustar carstencarstendjango-filter-23.5/django_filters/locale/nl/LC_MESSAGES/0000755000175000017500000000000014533556010022501 5ustar carstencarstendjango-filter-23.5/django_filters/locale/nl/LC_MESSAGES/django.po0000644000175000017500000000632714533556010024313 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-08-21 12:25+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Storm Heg \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: conf.py:16 msgid "date" msgstr "datum" #: conf.py:17 msgid "year" msgstr "jaar" #: conf.py:18 msgid "month" msgstr "maand" #: conf.py:19 msgid "day" msgstr "dag" #: conf.py:20 msgid "week day" msgstr "weekdag" #: conf.py:21 msgid "hour" msgstr "uur" #: conf.py:22 msgid "minute" msgstr "minuur" #: conf.py:23 msgid "second" msgstr "seconde" #: conf.py:27 conf.py:28 msgid "contains" msgstr "bevat" #: conf.py:29 msgid "is in" msgstr "zit in" #: conf.py:30 msgid "is greater than" msgstr "is groter dan" #: conf.py:31 msgid "is greater than or equal to" msgstr "is groter dan of gelijk aan" #: conf.py:32 msgid "is less than" msgstr "is minder dan" #: conf.py:33 msgid "is less than or equal to" msgstr "is minder dan of gelijk aan" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "begint met" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "eindigt met" #: conf.py:38 msgid "is in range" msgstr "zit in bereik" #: conf.py:39 msgid "is null" msgstr "is null" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "matcht regex" #: conf.py:42 conf.py:49 msgid "search" msgstr "zoek" #: conf.py:44 msgid "is contained by" msgstr "wordt bevat door" #: conf.py:45 msgid "overlaps" msgstr "overlapt" #: conf.py:46 msgid "has key" msgstr "heeft key" #: conf.py:47 msgid "has keys" msgstr "heeft keys" #: conf.py:48 msgid "has any keys" msgstr "heeft keys" #: fields.py:94 msgid "Select a lookup." msgstr "Selecteer een lookup." #: fields.py:198 msgid "Range query expects two values." msgstr "Bereik query verwacht twee waarden." #: filters.py:437 msgid "Today" msgstr "Vandaag" #: filters.py:438 msgid "Yesterday" msgstr "Gisteren" #: filters.py:439 msgid "Past 7 days" msgstr "Afgelopen 7 dagen" #: filters.py:440 msgid "This month" msgstr "Deze maand" #: filters.py:441 msgid "This year" msgstr "Dit jaar" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Meerdere waarden kunnen gescheiden worden door komma's." #: filters.py:721 tests/test_filters.py:1670 #, python-format msgid "%s (descending)" msgstr "%s (aflopend)" #: filters.py:737 msgid "Ordering" msgstr "Volgorde" #: rest_framework/filterset.py:33 #: templates/rest_framework/form.html:5 msgid "Submit" msgstr "Indienen" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Veld filters" #: utils.py:323 msgid "exclude" msgstr "uitsluiten" #: widgets.py:58 msgid "All" msgstr "Alles" #: widgets.py:162 msgid "Unknown" msgstr "Onbekend" #: widgets.py:162 msgid "Yes" msgstr "Ja" #: widgets.py:162 msgid "No" msgstr "Nee" django-filter-23.5/django_filters/locale/fa/0000755000175000017500000000000014533556010020671 5ustar carstencarstendjango-filter-23.5/django_filters/locale/fa/LC_MESSAGES/0000755000175000017500000000000014533556010022456 5ustar carstencarstendjango-filter-23.5/django_filters/locale/fa/LC_MESSAGES/django.po0000644000175000017500000000704714533556010024270 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: conf.py:16 msgid "date" msgstr "تاریخ" #: conf.py:17 msgid "year" msgstr "سال" #: conf.py:18 msgid "month" msgstr "ماه" #: conf.py:19 msgid "day" msgstr "روز" #: conf.py:20 msgid "week day" msgstr "روز هفته" #: conf.py:21 msgid "hour" msgstr "ساعت" #: conf.py:22 msgid "minute" msgstr "دقیقه" #: conf.py:23 msgid "second" msgstr "ثانیه" #: conf.py:27 conf.py:28 msgid "contains" msgstr "شامل" #: conf.py:29 msgid "is in" msgstr "هست در" #: conf.py:30 msgid "is greater than" msgstr "بزرگتر است از" #: conf.py:31 msgid "is greater than or equal to" msgstr "بزرگتر یا مساوی است" #: conf.py:32 msgid "is less than" msgstr "کوچکتر است از" #: conf.py:33 msgid "is less than or equal to" msgstr "کوچکتر یا مساوی است" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "شروع می شود با" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "به پایان می رسد با" #: conf.py:38 msgid "is in range" msgstr "در محدوده" #: conf.py:39 msgid "is null" msgstr "خالی است" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "با ریجکس منطبق است" #: conf.py:42 conf.py:49 msgid "search" msgstr "جستجو" #: conf.py:44 msgid "is contained by" msgstr "وجود دارد در" #: conf.py:45 msgid "overlaps" msgstr "تداخل دارد" #: conf.py:46 msgid "has key" msgstr "حاوی کلید است" #: conf.py:47 msgid "has keys" msgstr "حاوی کلیدها است" #: conf.py:48 msgid "has any keys" msgstr "حاوی هر کلیدی است" #: fields.py:94 msgid "Select a lookup." msgstr "یک لوک آپ را انتخاب کنید." #: fields.py:198 msgid "Range query expects two values." msgstr "محدوده کوئری دو مقدار را انتظار دارد." #: filters.py:437 msgid "Today" msgstr "امروز" #: filters.py:438 msgid "Yesterday" msgstr "دیروز" #: filters.py:439 msgid "Past 7 days" msgstr "۷ روز گذشته" #: filters.py:440 msgid "This month" msgstr "این ماه" #: filters.py:441 msgid "This year" msgstr "امسال" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "ممکن است چندین مقدار با کاما از هم جدا شوند." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (نزولی)" #: filters.py:737 msgid "Ordering" msgstr "مرتب سازی" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "ارسال" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "فیلترهای فیلد" #: utils.py:308 msgid "exclude" msgstr "به غیر از" #: widgets.py:58 msgid "All" msgstr "همه" #: widgets.py:162 msgid "Unknown" msgstr "ناشناس" #: widgets.py:162 msgid "Yes" msgstr "بله" #: widgets.py:162 msgid "No" msgstr "خیر" django-filter-23.5/django_filters/locale/es_AR/0000755000175000017500000000000014533556010021274 5ustar carstencarstendjango-filter-23.5/django_filters/locale/es_AR/LC_MESSAGES/0000755000175000017500000000000014533556010023061 5ustar carstencarstendjango-filter-23.5/django_filters/locale/es_AR/LC_MESSAGES/django.po0000644000175000017500000000573314533556010024673 0ustar carstencarsten# 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" django-filter-23.5/django_filters/locale/cs/0000755000175000017500000000000014533556010020710 5ustar carstencarstendjango-filter-23.5/django_filters/locale/cs/LC_MESSAGES/0000755000175000017500000000000014533556010022475 5ustar carstencarstendjango-filter-23.5/django_filters/locale/cs/LC_MESSAGES/django.po0000644000175000017500000000625214533556010024304 0ustar carstencarsten# msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2016-09-29 11:47+0300\n" "Last-Translator: Eugena Mikhaylikova \n" "Language-Team: TextTempearture\n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n " "<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" "X-Generator: Poedit 1.8.9\n" #: conf.py:16 msgid "date" msgstr "datum" #: conf.py:17 msgid "year" msgstr "rok" #: conf.py:18 msgid "month" msgstr "měsíc" #: conf.py:19 msgid "day" msgstr "den" #: conf.py:20 msgid "week day" msgstr "den v týdnu" #: conf.py:21 msgid "hour" msgstr "hodinu" #: conf.py:22 msgid "minute" msgstr "minutu" #: conf.py:23 msgid "second" msgstr "vteřina" #: conf.py:27 conf.py:28 msgid "contains" msgstr "obsahuje" #: conf.py:29 msgid "is in" msgstr "v" #: conf.py:30 msgid "is greater than" msgstr "více než" #: conf.py:31 msgid "is greater than or equal to" msgstr "větší nebo roven" #: conf.py:32 msgid "is less than" msgstr "méně než" #: conf.py:33 msgid "is less than or equal to" msgstr "menší nebo rovné" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "začíná" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "končí" #: conf.py:38 msgid "is in range" msgstr "v rozsahu" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "odpovídá normálnímu výrazu" #: conf.py:42 conf.py:49 msgid "search" msgstr "vyhledávání" #: conf.py:44 msgid "is contained by" msgstr "je obsažen v" #: conf.py:45 msgid "overlaps" msgstr "překrývají" #: conf.py:46 msgid "has key" msgstr "má klíč" #: conf.py:47 msgid "has keys" msgstr "má klíče" #: conf.py:48 msgid "has any keys" msgstr "má nějaké klíče" #: fields.py:94 msgid "Select a lookup." msgstr "" #: fields.py:198 msgid "Range query expects two values." msgstr "Rozsah dotazu očekává dvě hodnoty." #: filters.py:437 msgid "Today" msgstr "Dnes" #: filters.py:438 msgid "Yesterday" msgstr "Včera" #: filters.py:439 msgid "Past 7 days" msgstr "Posledních 7 dní" #: filters.py:440 msgid "This month" msgstr "Tento měsíc" #: filters.py:441 msgid "This year" msgstr "Tento rok" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Více hodnot lze oddělit čárkami." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (sestupně)" #: filters.py:737 msgid "Ordering" msgstr "Řád z" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Odeslat" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Filtry na polích" #: utils.py:308 msgid "exclude" msgstr "s výjimkou" #: widgets.py:58 msgid "All" msgstr "Všechno" #: widgets.py:162 msgid "Unknown" msgstr "Není nastaveno" #: widgets.py:162 msgid "Yes" msgstr "Ano" #: widgets.py:162 msgid "No" msgstr "Ne" #~ msgid "Any date" #~ msgstr "Jakékoliv datum" django-filter-23.5/django_filters/locale/be/0000755000175000017500000000000014533556010020671 5ustar carstencarstendjango-filter-23.5/django_filters/locale/be/LC_MESSAGES/0000755000175000017500000000000014533556010022456 5ustar carstencarstendjango-filter-23.5/django_filters/locale/be/LC_MESSAGES/django.po0000644000175000017500000000716014533556010024264 0ustar carstencarsten# msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2016-09-29 11:47+0300\n" "Last-Translator: Eugena Mikhaylikova \n" "Language-Team: TextTempearture\n" "Language: be\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" "%100>=11 && n%100<=14)? 2 : 3);\n" "X-Generator: Poedit 1.8.9\n" #: conf.py:16 msgid "date" msgstr "дата" #: conf.py:17 msgid "year" msgstr "год" #: conf.py:18 msgid "month" msgstr "месяц" #: conf.py:19 msgid "day" msgstr "дзень" #: conf.py:20 msgid "week day" msgstr "дзень тыдня" #: conf.py:21 msgid "hour" msgstr "гадзіну" #: conf.py:22 msgid "minute" msgstr "хвіліна" #: conf.py:23 msgid "second" msgstr "секунда" #: conf.py:27 conf.py:28 msgid "contains" msgstr "змяшчае" #: conf.py:29 msgid "is in" msgstr "у" #: conf.py:30 msgid "is greater than" msgstr "больш чым" #: conf.py:31 msgid "is greater than or equal to" msgstr "больш або роўна" #: conf.py:32 msgid "is less than" msgstr "менш чым" #: conf.py:33 msgid "is less than or equal to" msgstr "менш або роўна" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "пачынаецца" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "заканчваецца" #: conf.py:38 msgid "is in range" msgstr "у дыяпазоне" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "адпавядае рэгулярнаму выразу" #: conf.py:42 conf.py:49 msgid "search" msgstr "пошук" #: conf.py:44 msgid "is contained by" msgstr "змяшчаецца ў" #: conf.py:45 msgid "overlaps" msgstr "перакрываецца" #: conf.py:46 msgid "has key" msgstr "мае ключ" #: conf.py:47 msgid "has keys" msgstr "мае ключы" #: conf.py:48 msgid "has any keys" msgstr "мае любыя ключы" #: fields.py:94 msgid "Select a lookup." msgstr "" #: fields.py:198 msgid "Range query expects two values." msgstr "Запыт дыяпазону чакае два значэння." #: filters.py:437 msgid "Today" msgstr "Сёння" #: filters.py:438 msgid "Yesterday" msgstr "Учора" #: filters.py:439 msgid "Past 7 days" msgstr "Мінулыя 7 дзён" #: filters.py:440 msgid "This month" msgstr "За гэты месяц" #: filters.py:441 msgid "This year" msgstr "У гэтым годзе" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Некалькі значэнняў могуць быць падзеленыя коскамі." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (па змяншэнні)" #: filters.py:737 msgid "Ordering" msgstr "Парадак" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Адправіць" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Фільтры па палях" #: utils.py:308 msgid "exclude" msgstr "выключаючы" #: widgets.py:58 msgid "All" msgstr "Усе" #: widgets.py:162 msgid "Unknown" msgstr "Не было прапанавана" #: widgets.py:162 msgid "Yes" msgstr "Ды" #: widgets.py:162 msgid "No" msgstr "Няма" #~ msgid "Any date" #~ msgstr "Любая дата" django-filter-23.5/django_filters/locale/bg/0000755000175000017500000000000014533556010020673 5ustar carstencarstendjango-filter-23.5/django_filters/locale/bg/LC_MESSAGES/0000755000175000017500000000000014533556010022460 5ustar carstencarstendjango-filter-23.5/django_filters/locale/bg/LC_MESSAGES/django.po0000644000175000017500000000723414533556010024270 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Hristo Gatsinski , 2019. # msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2019-12-21 19:36+0200\n" "Last-Translator: Hristo Gatsinski \n" "Language-Team: \n" "Language: bg\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.8.9\n" #: conf.py:16 msgid "date" msgstr "дата" #: conf.py:17 msgid "year" msgstr "година" #: conf.py:18 msgid "month" msgstr "месец" #: conf.py:19 msgid "day" msgstr "ден" #: conf.py:20 msgid "week day" msgstr "ден от седмицата" #: conf.py:21 msgid "hour" msgstr "час" #: conf.py:22 msgid "minute" msgstr "минута" #: conf.py:23 msgid "second" msgstr "секунда" #: conf.py:27 conf.py:28 msgid "contains" msgstr "съдържа" #: conf.py:29 msgid "is in" msgstr "в" #: conf.py:30 msgid "is greater than" msgstr "е по-голям от" #: conf.py:31 msgid "is greater than or equal to" msgstr "е по-голям или равен на" #: conf.py:32 msgid "is less than" msgstr "е по-малък от" #: conf.py:33 msgid "is less than or equal to" msgstr "е по-малък или равен на" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "започва с" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "завършва с" #: conf.py:38 msgid "is in range" msgstr "е в диапазона" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "съвпада с регуларен израз" #: conf.py:42 conf.py:49 msgid "search" msgstr "търсене" #: conf.py:44 msgid "is contained by" msgstr "се съдържа от" #: conf.py:45 msgid "overlaps" msgstr "припокрива" #: conf.py:46 msgid "has key" msgstr "има ключ" #: conf.py:47 msgid "has keys" msgstr "има ключове" #: conf.py:48 msgid "has any keys" msgstr "има който и да е ключ" #: fields.py:94 msgid "Select a lookup." msgstr "Изберете справка" #: fields.py:198 msgid "Range query expects two values." msgstr "Търсенето по диапазон изисква две стойности" #: filters.py:437 msgid "Today" msgstr "Днес" #: filters.py:438 msgid "Yesterday" msgstr "Вчера" #: filters.py:439 msgid "Past 7 days" msgstr "Последните 7 дни" #: filters.py:440 msgid "This month" msgstr "Този месец" #: filters.py:441 msgid "This year" msgstr "Тази година" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Множество стойности може да се разделят със запетая" #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (намалавящ)" #: filters.py:737 msgid "Ordering" msgstr "Подредба" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Изпращане" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Филтри на полетата" #: utils.py:308 msgid "exclude" msgstr "изключва" #: widgets.py:58 msgid "All" msgstr "Всичко" #: widgets.py:162 msgid "Unknown" msgstr "Неизвестен" #: widgets.py:162 msgid "Yes" msgstr "Да" #: widgets.py:162 msgid "No" msgstr "Не" django-filter-23.5/django_filters/locale/de/0000755000175000017500000000000014533556010020673 5ustar carstencarstendjango-filter-23.5/django_filters/locale/de/LC_MESSAGES/0000755000175000017500000000000014533556010022460 5ustar carstencarstendjango-filter-23.5/django_filters/locale/de/LC_MESSAGES/django.po0000644000175000017500000000641214533556010024265 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2013-08-10 12:29+0100\n" "Last-Translator: Florian Apolloner \n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.5.4\n" #: conf.py:16 msgid "date" msgstr "Datum" #: conf.py:17 msgid "year" msgstr "Jahr" #: conf.py:18 msgid "month" msgstr "Monat" #: conf.py:19 msgid "day" msgstr "Tag" #: conf.py:20 msgid "week day" msgstr "Wochentag" #: conf.py:21 msgid "hour" msgstr "Stunde" #: conf.py:22 msgid "minute" msgstr "Minute" #: conf.py:23 msgid "second" msgstr "Sekunde" #: conf.py:27 conf.py:28 msgid "contains" msgstr "enthält" #: conf.py:29 msgid "is in" msgstr "ist in" #: conf.py:30 msgid "is greater than" msgstr "ist größer als" #: conf.py:31 msgid "is greater than or equal to" msgstr "ist größer oder gleich" #: conf.py:32 msgid "is less than" msgstr "ist kleiner als" #: conf.py:33 msgid "is less than or equal to" msgstr "ist kleiner oder gleich" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "beginnt mit" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "endet mit" #: conf.py:38 msgid "is in range" msgstr "ist im Bereich" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "passt auf Regex" #: conf.py:42 conf.py:49 msgid "search" msgstr "Suche" #: conf.py:44 msgid "is contained by" msgstr "ist enthalten in" #: conf.py:45 msgid "overlaps" msgstr "überlappen" #: conf.py:46 msgid "has key" msgstr "hat Schlüssel" #: conf.py:47 msgid "has keys" msgstr "hat Schlüssel" #: conf.py:48 msgid "has any keys" msgstr "hat beliebige Schlüssel" #: fields.py:94 msgid "Select a lookup." msgstr "" #: fields.py:198 msgid "Range query expects two values." msgstr "Die Bereichsabfrage erwartet zwei Werte." #: filters.py:437 msgid "Today" msgstr "Heute" #: filters.py:438 msgid "Yesterday" msgstr "Gestern" #: filters.py:439 msgid "Past 7 days" msgstr "Letzte 7 Tage" #: filters.py:440 msgid "This month" msgstr "Diesen Monat" #: filters.py:441 msgid "This year" msgstr "Dieses Jahr" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Mehrere Werte können durch Kommas getrennt sein." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (absteigend)" #: filters.py:737 msgid "Ordering" msgstr "Sortierung" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Absenden" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Feldfilter" #: utils.py:308 msgid "exclude" msgstr "ausschließen" #: widgets.py:58 msgid "All" msgstr "Alle" #: widgets.py:162 msgid "Unknown" msgstr "Unbekannte" #: widgets.py:162 msgid "Yes" msgstr "Ja" #: widgets.py:162 msgid "No" msgstr "Nein" #~ msgid "Any date" #~ msgstr "Alle Daten" django-filter-23.5/django_filters/locale/es/0000755000175000017500000000000014533556010020712 5ustar carstencarstendjango-filter-23.5/django_filters/locale/es/LC_MESSAGES/0000755000175000017500000000000014533556010022477 5ustar carstencarstendjango-filter-23.5/django_filters/locale/es/LC_MESSAGES/django.po0000644000175000017500000000654214533556010024310 0ustar carstencarsten# Django Filter translation. # Copyright (C) 2013 # This file is distributed under the same license as the django_filter package. # Carlos Goce, 2017. # Nicolás Stuardo, 2020 # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2023-02-12 14:36+0000\n" "Last-Translator: gallegonovato \n" "Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.16-dev\n" #: conf.py:16 msgid "date" msgstr "fecha" #: conf.py:17 msgid "year" msgstr "año" #: conf.py:18 msgid "month" msgstr "mes" #: conf.py:19 msgid "day" msgstr "día" #: conf.py:20 msgid "week day" msgstr "día de la semana" #: conf.py:21 msgid "hour" msgstr "hora" #: conf.py:22 msgid "minute" msgstr "minuto" #: conf.py:23 msgid "second" msgstr "segundo" #: conf.py:27 conf.py:28 msgid "contains" msgstr "contiene" #: conf.py:29 msgid "is in" msgstr "presente en" #: conf.py:30 msgid "is greater than" msgstr "mayor que" #: conf.py:31 msgid "is greater than or equal to" msgstr "mayor o igual que" #: conf.py:32 msgid "is less than" msgstr "menor que" #: conf.py:33 msgid "is less than or equal to" msgstr "menor o igual que" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "comienza por" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "termina por" #: conf.py:38 msgid "is in range" msgstr "en el rango" #: conf.py:39 msgid "is null" msgstr "es nulo" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "coincide con la expresión regular" #: conf.py:42 conf.py:49 msgid "search" msgstr "buscar" #: conf.py:44 msgid "is contained by" msgstr "contenido en" #: conf.py:45 msgid "overlaps" msgstr "solapado" #: conf.py:46 msgid "has key" msgstr "contiene la clave" #: conf.py:47 msgid "has keys" msgstr "contiene las claves" #: conf.py:48 msgid "has any keys" msgstr "contiene alguna de las claves" #: fields.py:94 msgid "Select a lookup." msgstr "Seleccione un operador de consulta." #: fields.py:198 msgid "Range query expects two values." msgstr "Consultar un rango requiere dos valores." #: filters.py:437 msgid "Today" msgstr "Hoy" #: filters.py:438 msgid "Yesterday" msgstr "Ayer" #: filters.py:439 msgid "Past 7 days" msgstr "Últimos 7 días" #: filters.py:440 msgid "This month" msgstr "Este mes" #: filters.py:441 msgid "This year" msgstr "Este año" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Múltiples valores separados por comas." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (descendente)" #: filters.py:737 msgid "Ordering" msgstr "Ordenado" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Enviar" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Filtros de campo" #: utils.py:308 msgid "exclude" msgstr "excluye" #: widgets.py:58 msgid "All" msgstr "Todo" #: widgets.py:162 msgid "Unknown" msgstr "Desconocido" #: widgets.py:162 msgid "Yes" msgstr "Sí" #: widgets.py:162 msgid "No" msgstr "No" #~ msgid "Any date" #~ msgstr "Cualquier fecha" django-filter-23.5/django_filters/locale/ar/0000755000175000017500000000000014533556010020705 5ustar carstencarstendjango-filter-23.5/django_filters/locale/ar/LC_MESSAGES/0000755000175000017500000000000014533556010022472 5ustar carstencarstendjango-filter-23.5/django_filters/locale/ar/LC_MESSAGES/django.po0000644000175000017500000000705114533556010024277 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # FULL NAME , 2020. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2020-03-24 00:48+0100\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" "X-Generator: Gtranslator 2.91.7\n" #: conf.py:16 msgid "date" msgstr "تاريخ" #: conf.py:17 msgid "year" msgstr "سنة" #: conf.py:18 msgid "month" msgstr "شهر" #: conf.py:19 msgid "day" msgstr "يوم" #: conf.py:20 msgid "week day" msgstr "يوم الأسبوع" #: conf.py:21 msgid "hour" msgstr "ساعة" #: conf.py:22 msgid "minute" msgstr "دقيقة" #: conf.py:23 msgid "second" msgstr "ثانية" #: conf.py:27 conf.py:28 msgid "contains" msgstr "يحتوي على" #: conf.py:29 msgid "is in" msgstr "في داخل" #: conf.py:30 msgid "is greater than" msgstr "أكبر من" #: conf.py:31 msgid "is greater than or equal to" msgstr "أكبر من أو يساوي" #: conf.py:32 msgid "is less than" msgstr "أصغر من" #: conf.py:33 msgid "is less than or equal to" msgstr "أصغر من أو يساوي" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "يبدأ ب" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "ينتهي ب" #: conf.py:38 msgid "is in range" msgstr "في النطاق" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "يطابق التعبير العادي" #: conf.py:42 conf.py:49 msgid "search" msgstr "بحث" #: conf.py:44 msgid "is contained by" msgstr "موجود في" #: conf.py:45 msgid "overlaps" msgstr "يتداخل" #: conf.py:46 msgid "has key" msgstr "لديه مفتاح" #: conf.py:47 msgid "has keys" msgstr "لديه مفاتيح" #: conf.py:48 msgid "has any keys" msgstr "لديه أي مفاتيح" #: fields.py:94 msgid "Select a lookup." msgstr "حدد بحث" #: fields.py:198 msgid "Range query expects two values." msgstr "إستعلام النطاق يتوقع قيمتين" #: filters.py:437 msgid "Today" msgstr "اليوم" #: filters.py:438 msgid "Yesterday" msgstr "أمس" #: filters.py:439 msgid "Past 7 days" msgstr "الأيام السبعة الماضية" #: filters.py:440 msgid "This month" msgstr "هذا الشهر" #: filters.py:441 msgid "This year" msgstr "هذه السنة" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "يمكن فصل القيم المتعددة بفواصل." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (تنازلي)" #: filters.py:737 msgid "Ordering" msgstr "الترتيب" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "إرسال" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "مرشحات الحقل" #: utils.py:308 msgid "exclude" msgstr "استبعاد" #: widgets.py:58 msgid "All" msgstr "كل" #: widgets.py:162 msgid "Unknown" msgstr "مجهول" #: widgets.py:162 msgid "Yes" msgstr "نعم" #: widgets.py:162 msgid "No" msgstr "لا" django-filter-23.5/django_filters/locale/sk/0000755000175000017500000000000014533556010020720 5ustar carstencarstendjango-filter-23.5/django_filters/locale/sk/LC_MESSAGES/0000755000175000017500000000000014533556010022505 5ustar carstencarstendjango-filter-23.5/django_filters/locale/sk/LC_MESSAGES/django.po0000644000175000017500000000677614533556010024327 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2023-07-21 19:07+0000\n" "Last-Translator: Milan Šalka \n" "Language-Team: Slovak \n" "Language: sk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n " ">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n" "X-Generator: Weblate 5.0-dev\n" "X-Translated-Using: django-rosetta 0.8.1\n" #: conf.py:16 msgid "date" msgstr "dátum" #: conf.py:17 msgid "year" msgstr "rok" #: conf.py:18 msgid "month" msgstr "mesiac" #: conf.py:19 msgid "day" msgstr "deň" #: conf.py:20 msgid "week day" msgstr "deň týždňa" #: conf.py:21 msgid "hour" msgstr "hodina" #: conf.py:22 msgid "minute" msgstr "minúta" #: conf.py:23 msgid "second" msgstr "sekunda" #: conf.py:27 conf.py:28 msgid "contains" msgstr "obsahuje" #: conf.py:29 msgid "is in" msgstr "je v" #: conf.py:30 msgid "is greater than" msgstr "je vačší než" #: conf.py:31 msgid "is greater than or equal to" msgstr "je vačší alebo rovný ako" #: conf.py:32 msgid "is less than" msgstr "je menší než" #: conf.py:33 msgid "is less than or equal to" msgstr "je menší alebo rovný ako" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "začína s" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "končí s" #: conf.py:38 msgid "is in range" msgstr "je v rozsahu" #: conf.py:39 msgid "is null" msgstr "je nulová" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "spĺňa regex" #: conf.py:42 conf.py:49 msgid "search" msgstr "hľadať" #: conf.py:44 msgid "is contained by" msgstr "je obsiahnutý" #: conf.py:45 msgid "overlaps" msgstr "presahuje" #: conf.py:46 msgid "has key" msgstr "má kľúč" #: conf.py:47 msgid "has keys" msgstr "má kľúče" #: conf.py:48 msgid "has any keys" msgstr "má akékoľvek kľúče" #: fields.py:94 msgid "Select a lookup." msgstr "Vyberte vyhľadávanie." #: fields.py:198 msgid "Range query expects two values." msgstr "Rozsah očakáva dve hodnoty." #: filters.py:437 msgid "Today" msgstr "Dnes" #: filters.py:438 msgid "Yesterday" msgstr "Včera" #: filters.py:439 msgid "Past 7 days" msgstr "Posledných 7 dní" #: filters.py:440 msgid "This month" msgstr "Tento mesiac" #: filters.py:441 msgid "This year" msgstr "Tento rok" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Viacero hodnôt môže byť oddelených čiarkami." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (klesajúco)" #: filters.py:737 msgid "Ordering" msgstr "Zoradenie" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Potvrdiť" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Filtre poľa" #: utils.py:308 msgid "exclude" msgstr "neobsahuje" #: widgets.py:58 msgid "All" msgstr "Všetky" #: widgets.py:162 msgid "Unknown" msgstr "Neznáme" #: widgets.py:162 msgid "Yes" msgstr "Áno" #: widgets.py:162 msgid "No" msgstr "Nie" #~ msgid "Any date" #~ msgstr "Akýkoľvek dátum" django-filter-23.5/django_filters/locale/fr/0000755000175000017500000000000014533556010020712 5ustar carstencarstendjango-filter-23.5/django_filters/locale/fr/LC_MESSAGES/0000755000175000017500000000000014533556010022477 5ustar carstencarstendjango-filter-23.5/django_filters/locale/fr/LC_MESSAGES/django.po0000644000175000017500000000654414533556010024312 0ustar carstencarsten# Django Filter translation. # Copyright (C) 2013 # This file is distributed under the same license as the django_filter package. # Axel Haustant , 2013. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2013-07-05 19:24+0200\n" "Last-Translator: Axel Haustant \n" "Language-Team: LANGUAGE \n" "Language: French\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 "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 lookup" #: fields.py:198 msgid "Range query expects two values." msgstr "La fouchette 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" django-filter-23.5/django_filters/locale/da/0000755000175000017500000000000014533556010020667 5ustar carstencarstendjango-filter-23.5/django_filters/locale/da/LC_MESSAGES/0000755000175000017500000000000014533556010022454 5ustar carstencarstendjango-filter-23.5/django_filters/locale/da/LC_MESSAGES/django.po0000644000175000017500000000614514533556010024264 0ustar carstencarstenmsgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2017-10-28\n" "Last-Translator: Danni Randeris \n" "Language-Team: Danni Randeris \n" "Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.0.1\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: conf.py:16 msgid "date" msgstr "dato" #: conf.py:17 msgid "year" msgstr "år" #: conf.py:18 msgid "month" msgstr "måned" #: conf.py:19 msgid "day" msgstr "dag" #: conf.py:20 msgid "week day" msgstr "ugedag" #: conf.py:21 msgid "hour" msgstr "time" #: conf.py:22 msgid "minute" msgstr "minut" #: conf.py:23 msgid "second" msgstr "sekund" #: conf.py:27 conf.py:28 msgid "contains" msgstr "indeholder" #: conf.py:29 msgid "is in" msgstr "er i" #: conf.py:30 msgid "is greater than" msgstr "er større end" #: conf.py:31 msgid "is greater than or equal to" msgstr "er større end eller lig med" #: conf.py:32 msgid "is less than" msgstr "er mindre end" #: conf.py:33 msgid "is less than or equal to" msgstr "er mindre end eller lig med" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "starter med" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "slutter med" #: conf.py:38 msgid "is in range" msgstr "er i intervallet" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "matcher regex" #: conf.py:42 conf.py:49 msgid "search" msgstr "søg" #: conf.py:44 msgid "is contained by" msgstr "er indeholdt af" #: conf.py:45 msgid "overlaps" msgstr "overlapper" #: conf.py:46 msgid "has key" msgstr "har string" #: conf.py:47 msgid "has keys" msgstr "har stringe" #: conf.py:48 msgid "has any keys" msgstr "har hvilken som helst string" #: fields.py:94 msgid "Select a lookup." msgstr "" #: fields.py:198 msgid "Range query expects two values." msgstr "Interval forespørgslen forventer to værdier." #: filters.py:437 msgid "Today" msgstr "I dag" #: filters.py:438 msgid "Yesterday" msgstr "I går" #: filters.py:439 msgid "Past 7 days" msgstr "Sidste 7 dage" #: filters.py:440 msgid "This month" msgstr "Denne måned" #: filters.py:441 msgid "This year" msgstr "Dette år" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Flere værdier kan adskilles via komma." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (aftagende)" #: filters.py:737 msgid "Ordering" msgstr "Sortering" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 #, fuzzy msgid "Submit" msgstr "Indsend" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 #, fuzzy msgid "Field filters" msgstr "Felt filtre" #: utils.py:308 msgid "exclude" msgstr "udelad" #: widgets.py:58 msgid "All" msgstr "Alle" #: widgets.py:162 msgid "Unknown" msgstr "Ukendt" #: widgets.py:162 msgid "Yes" msgstr "Ja" #: widgets.py:162 msgid "No" msgstr "Nej" #~ msgid "Any date" #~ msgstr "Hvilken som helst dag" django-filter-23.5/django_filters/locale/zh_CN/0000755000175000017500000000000014533556010021304 5ustar carstencarstendjango-filter-23.5/django_filters/locale/zh_CN/LC_MESSAGES/0000755000175000017500000000000014533556010023071 5ustar carstencarstendjango-filter-23.5/django_filters/locale/zh_CN/LC_MESSAGES/django.po0000644000175000017500000000643214533556010024700 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Kane Blueriver , 2016. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2023-05-07 03:57+0000\n" "Last-Translator: Lattefang <370358679@qq.com>\n" "Language-Team: Chinese (Simplified) \n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.18-dev\n" #: conf.py:16 msgid "date" msgstr "日期" #: conf.py:17 msgid "year" msgstr "年" #: conf.py:18 msgid "month" msgstr "月" #: conf.py:19 msgid "day" msgstr "日" #: conf.py:20 msgid "week day" msgstr "工作日" #: conf.py:21 msgid "hour" msgstr "小时" #: conf.py:22 msgid "minute" msgstr "分钟" #: conf.py:23 msgid "second" msgstr "秒" #: conf.py:27 conf.py:28 msgid "contains" msgstr "包含" #: conf.py:29 msgid "is in" msgstr "在" #: conf.py:30 msgid "is greater than" msgstr "大于" #: conf.py:31 msgid "is greater than or equal to" msgstr "大于等于" #: conf.py:32 msgid "is less than" msgstr "小于" #: conf.py:33 msgid "is less than or equal to" msgstr "小于等于" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "以……开始" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "以……结尾" #: conf.py:38 msgid "is in range" msgstr "在范围内" #: conf.py:39 msgid "is null" msgstr "为空" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "匹配正则表达式" #: conf.py:42 conf.py:49 msgid "search" msgstr "搜索" #: conf.py:44 msgid "is contained by" msgstr "包含在" #: conf.py:45 msgid "overlaps" msgstr "重叠" #: conf.py:46 msgid "has key" msgstr "单值" #: conf.py:47 msgid "has keys" msgstr "多值" #: conf.py:48 msgid "has any keys" msgstr "任何值" #: fields.py:94 msgid "Select a lookup." msgstr "选择查找。" #: fields.py:198 msgid "Range query expects two values." msgstr "范围查询需要两个值。" #: filters.py:437 msgid "Today" msgstr "今日" #: filters.py:438 msgid "Yesterday" msgstr "昨日" #: filters.py:439 msgid "Past 7 days" msgstr "过去 7 日" #: filters.py:440 msgid "This month" msgstr "本月" #: filters.py:441 msgid "This year" msgstr "今年" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "多个值可以用逗号分隔。" #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s(降序)" #: filters.py:737 msgid "Ordering" msgstr "排序" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "提交" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "字段过滤器" #: utils.py:308 msgid "exclude" msgstr "排除" #: widgets.py:58 msgid "All" msgstr "全部" #: widgets.py:162 msgid "Unknown" msgstr "未知" #: widgets.py:162 msgid "Yes" msgstr "是" #: widgets.py:162 msgid "No" msgstr "否" #~ msgid "This is an exclusion filter" #~ msgstr "未启用该过滤器" django-filter-23.5/django_filters/locale/ru/0000755000175000017500000000000014533556010020731 5ustar carstencarstendjango-filter-23.5/django_filters/locale/ru/LC_MESSAGES/0000755000175000017500000000000014533556010022516 5ustar carstencarstendjango-filter-23.5/django_filters/locale/ru/LC_MESSAGES/django.po0000644000175000017500000000742714533556010024332 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2016-09-29 11:47+0300\n" "Last-Translator: Mikhail Mitrofanov \n" "Language-Team: \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" "%100>=11 && n%100<=14)? 2 : 3);\n" "X-Generator: Poedit 1.8.9\n" #: conf.py:16 msgid "date" msgstr "дата" #: conf.py:17 msgid "year" msgstr "год" #: conf.py:18 msgid "month" msgstr "месяц" #: conf.py:19 msgid "day" msgstr "день" #: conf.py:20 msgid "week day" msgstr "день недели" #: conf.py:21 msgid "hour" msgstr "час" #: conf.py:22 msgid "minute" msgstr "минута" #: conf.py:23 msgid "second" msgstr "секунда" #: conf.py:27 conf.py:28 msgid "contains" msgstr "содержит" #: conf.py:29 msgid "is in" msgstr "в" #: conf.py:30 msgid "is greater than" msgstr "больше чем" #: conf.py:31 msgid "is greater than or equal to" msgstr "больше или равно" #: conf.py:32 msgid "is less than" msgstr "меньше чем" #: conf.py:33 msgid "is less than or equal to" msgstr "меньше или равно" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "начинается" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "заканчивается" #: conf.py:38 msgid "is in range" msgstr "в диапазоне" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "соответствует регулярному выражению" #: conf.py:42 conf.py:49 msgid "search" msgstr "поиск" #: conf.py:44 msgid "is contained by" msgstr "содержится в" #: conf.py:45 msgid "overlaps" msgstr "перекрывается" #: conf.py:46 msgid "has key" msgstr "имеет ключ" #: conf.py:47 msgid "has keys" msgstr "имеет ключи" #: conf.py:48 msgid "has any keys" msgstr "имеет любые ключи" #: fields.py:94 msgid "Select a lookup." msgstr "" #: fields.py:198 msgid "Range query expects two values." msgstr "Запрос диапазона ожидает два значения." #: filters.py:437 msgid "Today" msgstr "Сегодня" #: filters.py:438 msgid "Yesterday" msgstr "Вчера" #: filters.py:439 msgid "Past 7 days" msgstr "Прошедшие 7 дней" #: filters.py:440 msgid "This month" msgstr "За этот месяц" #: filters.py:441 msgid "This year" msgstr "В этом году" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Несколько значений могут быть разделены запятыми." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (по убыванию)" #: filters.py:737 msgid "Ordering" msgstr "Порядок" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Отправить" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Фильтры по полям" #: utils.py:308 msgid "exclude" msgstr "исключая" #: widgets.py:58 msgid "All" msgstr "Все" #: widgets.py:162 msgid "Unknown" msgstr "Не задано" #: widgets.py:162 msgid "Yes" msgstr "Да" #: widgets.py:162 msgid "No" msgstr "Нет" #~ msgid "Any date" #~ msgstr "Любая дата" django-filter-23.5/django_filters/locale/pl/0000755000175000017500000000000014533556010020716 5ustar carstencarstendjango-filter-23.5/django_filters/locale/pl/LC_MESSAGES/0000755000175000017500000000000014533556010022503 5ustar carstencarstendjango-filter-23.5/django_filters/locale/pl/LC_MESSAGES/django.po0000644000175000017500000000721014533556010024305 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: django_filters 0.0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2023-04-10 20:47+0000\n" "Last-Translator: Quadric \n" "Language-Team: Polish \n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" "%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" "%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" "X-Generator: Weblate 4.17-dev\n" #: conf.py:16 msgid "date" msgstr "data" #: conf.py:17 msgid "year" msgstr "rok" #: conf.py:18 msgid "month" msgstr "miesiąc" #: conf.py:19 msgid "day" msgstr "dzień" #: conf.py:20 msgid "week day" msgstr "dzień tygodnia" #: conf.py:21 msgid "hour" msgstr "godzina" #: conf.py:22 msgid "minute" msgstr "minuta" #: conf.py:23 msgid "second" msgstr "sekunda" #: conf.py:27 conf.py:28 msgid "contains" msgstr "zawiera" #: conf.py:29 msgid "is in" msgstr "zawiera się w" #: conf.py:30 msgid "is greater than" msgstr "powyżej" #: conf.py:31 msgid "is greater than or equal to" msgstr "powyżej lub równe" #: conf.py:32 msgid "is less than" msgstr "poniżej" #: conf.py:33 msgid "is less than or equal to" msgstr "poniżej lub równe" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "zaczyna się od" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "kończy się na" #: conf.py:38 msgid "is in range" msgstr "zawiera się w zakresie" #: conf.py:39 msgid "is null" msgstr "jest wartością null" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "pasuje do wyrażenia regularnego" #: conf.py:42 conf.py:49 msgid "search" msgstr "szukaj" #: conf.py:44 msgid "is contained by" msgstr "zawiera się w" #: conf.py:45 msgid "overlaps" msgstr "nakłada się" #: conf.py:46 msgid "has key" msgstr "posiada klucz" #: conf.py:47 msgid "has keys" msgstr "posiada klucze" #: conf.py:48 msgid "has any keys" msgstr "posiada jakiekolwiek klucze" #: fields.py:94 msgid "Select a lookup." msgstr "Wybierz wyszukiwanie." #: fields.py:198 msgid "Range query expects two values." msgstr "Zapytanie o zakres oczekuje dwóch wartości." #: filters.py:437 msgid "Today" msgstr "Dziś" #: filters.py:438 msgid "Yesterday" msgstr "Wczoraj" #: filters.py:439 msgid "Past 7 days" msgstr "Ostatnie 7 dni" #: filters.py:440 msgid "This month" msgstr "Ten miesiąc" #: filters.py:441 msgid "This year" msgstr "Ten rok" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Wiele wartości można rozdzielić przecinkami." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (malejąco)" #: filters.py:737 msgid "Ordering" msgstr "Sortowanie" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Wyślij" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Filtry pola" #: utils.py:308 msgid "exclude" msgstr "wyklucz" #: widgets.py:58 msgid "All" msgstr "Wszystko" #: widgets.py:162 msgid "Unknown" msgstr "Nieznane" #: widgets.py:162 msgid "Yes" msgstr "Tak" #: widgets.py:162 msgid "No" msgstr "Nie" #~ msgid "Any date" #~ msgstr "Dowolna data" #~ msgid "This is an exclusion filter" #~ msgstr "Jest to filtr wykluczający" django-filter-23.5/django_filters/locale/it/0000755000175000017500000000000014533556010020717 5ustar carstencarstendjango-filter-23.5/django_filters/locale/it/LC_MESSAGES/0000755000175000017500000000000014533556010022504 5ustar carstencarstendjango-filter-23.5/django_filters/locale/it/LC_MESSAGES/django.po0000644000175000017500000000646414533556010024320 0ustar carstencarsten# Django Filter translation. # Copyright (C) 2013 # This file is distributed under the same license as the django_filter package. # Carlos Goce, 2017. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2023-06-11 16:51+0000\n" "Last-Translator: Daniele Tricoli \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 4.18-dev\n" #: conf.py:16 msgid "date" msgstr "data" #: conf.py:17 msgid "year" msgstr "anno" #: conf.py:18 msgid "month" msgstr "mese" #: conf.py:19 msgid "day" msgstr "giorno" #: conf.py:20 msgid "week day" msgstr "giorno della settimana" #: conf.py:21 msgid "hour" msgstr "ora" #: conf.py:22 msgid "minute" msgstr "minuto" #: conf.py:23 msgid "second" msgstr "secondo" #: conf.py:27 conf.py:28 msgid "contains" msgstr "contiene" #: conf.py:29 msgid "is in" msgstr "presente in" #: conf.py:30 msgid "is greater than" msgstr "maggiore di" #: conf.py:31 msgid "is greater than or equal to" msgstr "maggiore o uguale di" #: conf.py:32 msgid "is less than" msgstr "minore di" #: conf.py:33 msgid "is less than or equal to" msgstr "minore o uguale di" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "comincia per" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "termina per" #: conf.py:38 msgid "is in range" msgstr "nell'intervallo" #: conf.py:39 msgid "is null" msgstr "nullo" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "coincide con la espressione regolare" #: conf.py:42 conf.py:49 msgid "search" msgstr "cerca" #: conf.py:44 msgid "is contained by" msgstr "contenuto in" #: conf.py:45 msgid "overlaps" msgstr "sovrapposto" #: conf.py:46 msgid "has key" msgstr "contiene la chiave" #: conf.py:47 msgid "has keys" msgstr "contiene le chiavi" #: conf.py:48 msgid "has any keys" msgstr "contiene qualsiasi chiave" #: fields.py:94 msgid "Select a lookup." msgstr "" #: fields.py:198 msgid "Range query expects two values." msgstr "La query di intervallo richiede due valori." #: filters.py:437 msgid "Today" msgstr "Oggi" #: filters.py:438 msgid "Yesterday" msgstr "Ieri" #: filters.py:439 msgid "Past 7 days" msgstr "Ultimi 7 giorni" #: filters.py:440 msgid "This month" msgstr "Questo mese" #: filters.py:441 msgid "This year" msgstr "Questo anno" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Più valori separati da virgole." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (decrescente)" #: filters.py:737 msgid "Ordering" msgstr "Ordinamento" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Invia" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Filtri del campo" #: utils.py:308 msgid "exclude" msgstr "escludi" #: widgets.py:58 msgid "All" msgstr "Tutti" #: widgets.py:162 msgid "Unknown" msgstr "Sconosciuto" #: widgets.py:162 msgid "Yes" msgstr "Sì" #: widgets.py:162 msgid "No" msgstr "No" #~ msgid "Any date" #~ msgstr "Qualsiasi data" django-filter-23.5/django_filters/locale/el/0000755000175000017500000000000014533556010020703 5ustar carstencarstendjango-filter-23.5/django_filters/locale/el/LC_MESSAGES/0000755000175000017500000000000014533556010022470 5ustar carstencarstendjango-filter-23.5/django_filters/locale/el/LC_MESSAGES/django.po0000644000175000017500000000750514533556010024301 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Serafeim Papastefanos , 2017. # msgid "" msgstr "" "Project-Id-Version: django-filter\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 11:07+0000\n" "PO-Revision-Date: 2017-11-16 10:04+0200\n" "Last-Translator: Serafeim Papastefanos \n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.5\n" #: conf.py:16 msgid "date" msgstr "ημερομηνία" #: conf.py:17 msgid "year" msgstr "έτος" #: conf.py:18 msgid "month" msgstr "μήνας" #: conf.py:19 msgid "day" msgstr "ημέρα" #: conf.py:20 msgid "week day" msgstr "ημέρα της εβδομάδας" #: conf.py:21 msgid "hour" msgstr "ώρα" #: conf.py:22 msgid "minute" msgstr "λεπτό" #: conf.py:23 msgid "second" msgstr "δευτερόλεπτο" #: conf.py:27 conf.py:28 msgid "contains" msgstr "περιέχει" #: conf.py:29 msgid "is in" msgstr "είναι εντός των" #: conf.py:30 msgid "is greater than" msgstr "είναι μεγαλύτερο από" #: conf.py:31 msgid "is greater than or equal to" msgstr "είναι μεγαλύτερο ή ίσο του" #: conf.py:32 msgid "is less than" msgstr "είναι μικρότερο από" #: conf.py:33 msgid "is less than or equal to" msgstr "είναι μικρότερο ή ίσο του" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "ξεκινά με" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "τελειώνει με" #: conf.py:38 msgid "is in range" msgstr "είναι εντος του εύρους" #: conf.py:39 msgid "is null" msgstr "" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "περιέχει regex" #: conf.py:42 conf.py:49 msgid "search" msgstr "αναζήτηση" #: conf.py:44 msgid "is contained by" msgstr "περιέχεται σε" #: conf.py:45 msgid "overlaps" msgstr "επικαλύπτεται" #: conf.py:46 msgid "has key" msgstr "έχει το κλειδί" #: conf.py:47 msgid "has keys" msgstr "έχει τα κλειδιά" #: conf.py:48 msgid "has any keys" msgstr "έχει οποιαδήποτε κλειδιά" #: fields.py:94 msgid "Select a lookup." msgstr "" #: fields.py:198 msgid "Range query expects two values." msgstr "Το ερώτημα εύρους απαιτεί δύο τιμές," #: filters.py:437 msgid "Today" msgstr "Σήμερα" #: filters.py:438 msgid "Yesterday" msgstr "Χτες" #: filters.py:439 msgid "Past 7 days" msgstr "Τις προηγούμενες 7 ημέρες" #: filters.py:440 msgid "This month" msgstr "Αυτό το μήνα" #: filters.py:441 msgid "This year" msgstr "Αυτό το έτος" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Οι πολλαπλές τιμές πρέπει να διαχωρίζονται με κόμμα." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (φθίνουσα" #: filters.py:737 msgid "Ordering" msgstr "Ταξινόμηση" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Υποβολή" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Φίλτρα πεδίων" #: utils.py:308 msgid "exclude" msgstr "απέκλεισε" #: widgets.py:58 msgid "All" msgstr "Όλα" #: widgets.py:162 msgid "Unknown" msgstr "Άγνωστο" #: widgets.py:162 msgid "Yes" msgstr "Ναι" #: widgets.py:162 msgid "No" msgstr "Όχι" #~ msgid "Any date" #~ msgstr "Οποιαδήποτε ημερομηνία" django-filter-23.5/django_filters/locale/ro/0000755000175000017500000000000014533556010020723 5ustar carstencarstendjango-filter-23.5/django_filters/locale/ro/LC_MESSAGES/0000755000175000017500000000000014533556010022510 5ustar carstencarstendjango-filter-23.5/django_filters/locale/ro/LC_MESSAGES/django.po0000644000175000017500000000662614533556010024324 0ustar carstencarsten# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-02-10 14:47+0000\n" "PO-Revision-Date: 2023-02-10 16:28+0000\n" "Last-Translator: Dan Braghis \n" "Language-Team: Romanian \n" "Language: ro\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " "20)) ? 1 : 2;\n" "X-Generator: Weblate 4.16-dev\n" #: conf.py:16 msgid "date" msgstr "dată" #: conf.py:17 msgid "year" msgstr "an" #: conf.py:18 msgid "month" msgstr "lună" #: conf.py:19 msgid "day" msgstr "zi" #: conf.py:20 msgid "week day" msgstr "zi a săptămânii" #: conf.py:21 msgid "hour" msgstr "oră" #: conf.py:22 msgid "minute" msgstr "minută" #: conf.py:23 msgid "second" msgstr "secundă" #: conf.py:27 conf.py:28 msgid "contains" msgstr "conține" #: conf.py:29 msgid "is in" msgstr "este în" #: conf.py:30 msgid "is greater than" msgstr "este mai mare decât" #: conf.py:31 msgid "is greater than or equal to" msgstr "este mai mare sau egal cu" #: conf.py:32 msgid "is less than" msgstr "este mai mic decât" #: conf.py:33 msgid "is less than or equal to" msgstr "este mai mic sau egal cu" #: conf.py:34 conf.py:35 msgid "starts with" msgstr "începe cu" #: conf.py:36 conf.py:37 msgid "ends with" msgstr "se termină cu" #: conf.py:38 msgid "is in range" msgstr "este în intervalul" #: conf.py:39 msgid "is null" msgstr "este nul" #: conf.py:40 conf.py:41 msgid "matches regex" msgstr "se potrivește cu expresia regex" #: conf.py:42 conf.py:49 msgid "search" msgstr "căutare" #: conf.py:44 msgid "is contained by" msgstr "cuprins de" #: conf.py:45 msgid "overlaps" msgstr "se suprapune" #: conf.py:46 msgid "has key" msgstr "are cheia" #: conf.py:47 msgid "has keys" msgstr "are cheile" #: conf.py:48 msgid "has any keys" msgstr "are orice cheie" #: fields.py:94 msgid "Select a lookup." msgstr "Selectați o căutare." #: fields.py:198 msgid "Range query expects two values." msgstr "Interogarea de interval așteaptă două valori." #: filters.py:437 msgid "Today" msgstr "Astăzi" #: filters.py:438 msgid "Yesterday" msgstr "Ieri" #: filters.py:439 msgid "Past 7 days" msgstr "Ultimele 7 zile" #: filters.py:440 msgid "This month" msgstr "Luna aceasta" #: filters.py:441 msgid "This year" msgstr "Anul acesta" #: filters.py:543 msgid "Multiple values may be separated by commas." msgstr "Valorile multiple pot fi separate prin virgule." #: filters.py:721 #, python-format msgid "%s (descending)" msgstr "%s (descescător)" #: filters.py:737 msgid "Ordering" msgstr "Rânduire" #: rest_framework/filterset.py:33 #: templates/django_filters/rest_framework/form.html:5 msgid "Submit" msgstr "Trimite" #: templates/django_filters/rest_framework/crispy_form.html:4 #: templates/django_filters/rest_framework/form.html:2 msgid "Field filters" msgstr "Filtre de câmp" #: utils.py:312 msgid "exclude" msgstr "exclude" #: widgets.py:58 msgid "All" msgstr "Toate" #: widgets.py:162 msgid "Unknown" msgstr "Necunoscut" #: widgets.py:162 msgid "Yes" msgstr "Da" #: widgets.py:162 msgid "No" msgstr "Nu" django-filter-23.5/django_filters/conf.py0000644000175000017500000000575114533556010020353 0ustar carstencarstenfrom 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) django-filter-23.5/django_filters/filterset.py0000644000175000017500000003707014533556010021426 0ustar carstencarstenimport 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 .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 {} self.queryset = queryset self.request = request self.form_prefix = prefix self.filters = copy.deepcopy(self.base_filters) # propagate the model and filterset to the filters for filter_ in self.filters.values(): filter_.model = model filter_.parent = self def is_valid(self): """ Return True if the underlying form has no errors, or False otherwise. """ return self.is_bound and self.form.is_valid() @property def errors(self): """ Return an ErrorDict for the data provided for the underlying form. """ return self.form.errors def filter_queryset(self, queryset): """ Filter the queryset with the underlying form's `cleaned_data`. You must call `is_valid()` or `errors` before calling this method. This method should be overridden if additional filtering needs to be applied to the queryset before it is cached. """ for name, value in self.form.cleaned_data.items(): queryset = self.filters[name].filter(queryset, value) assert isinstance( queryset, models.QuerySet ), "Expected '%s.%s' to return a QuerySet, but got a %s instead." % ( type(self).__name__, name, type(queryset).__name__, ) return queryset @property def qs(self): if not hasattr(self, "_qs"): qs = self.queryset.all() if self.is_bound: # ensure form validation before filtering self.errors qs = self.filter_queryset(qs) self._qs = qs return self._qs def get_form_class(self): """ Returns a django Form suitable of validating the filterset data. This method should be overridden if the form class needs to be customized relative to the filterset instance. """ fields = OrderedDict( [(name, filter_.field) for name, filter_ in self.filters.items()] ) return type(str("%sForm" % self.__class__.__name__), (self._meta.form,), fields) @property def form(self): if not hasattr(self, "_form"): Form = self.get_form_class() if self.is_bound: self._form = Form(self.data, prefix=self.form_prefix) else: self._form = Form(prefix=self.form_prefix) return self._form @classmethod def get_fields(cls): """ Resolve the 'fields' argument that should be used for generating filters on the filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'. """ model = cls._meta.model fields = cls._meta.fields exclude = cls._meta.exclude assert not (fields is None and exclude is None), ( "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " "has been deprecated since 0.15.0 and is now disallowed. Add an explicit " "'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__ ) # Setting exclude with no fields implies all other fields. if exclude is not None and fields is None: fields = ALL_FIELDS # Resolve ALL_FIELDS into all fields for the filterset's model. if fields == ALL_FIELDS: fields = get_all_model_fields(model) # Remove excluded fields exclude = exclude or [] if not isinstance(fields, dict): fields = [ (f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude ] else: fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude] return OrderedDict(fields) @classmethod def get_filter_name(cls, field_name, lookup_expr): """ Combine a field name and lookup expression into a usable filter name. Exact lookups are the implicit default, so "exact" is stripped from the end of the filter name. """ filter_name = LOOKUP_SEP.join([field_name, lookup_expr]) # This also works with transformed exact lookups, such as 'date__exact' _default_expr = LOOKUP_SEP + settings.DEFAULT_LOOKUP_EXPR if filter_name.endswith(_default_expr): filter_name = filter_name[: -len(_default_expr)] return filter_name @classmethod def get_filters(cls): """ Get all filters for the filterset. This is the combination of declared and generated filters. """ # No model specified - skip filter generation if not cls._meta.model: return cls.declared_filters.copy() # Determine the filters that should be included on the filterset. filters = OrderedDict() fields = cls.get_fields() undefined = [] for field_name, lookups in fields.items(): field = get_model_field(cls._meta.model, field_name) # warn if the field doesn't exist. if field is None: undefined.append(field_name) for lookup_expr in lookups: filter_name = cls.get_filter_name(field_name, lookup_expr) # If the filter is explicitly declared on the class, skip generation if filter_name in cls.declared_filters: filters[filter_name] = cls.declared_filters[filter_name] continue if field is not None: filters[filter_name] = cls.filter_for_field( field, field_name, lookup_expr ) # Allow Meta.fields to contain declared filters *only* when a list/tuple if isinstance(cls._meta.fields, (list, tuple)): undefined = [f for f in undefined if f not in cls.declared_filters] if undefined: raise TypeError( "'Meta.fields' must not contain non-model field names: %s" % ", ".join(undefined) ) # Add in declared filters. This is necessary since we don't enforce adding # declared filters to the 'Meta.fields' option filters.update(cls.declared_filters) return filters @classmethod def filter_for_field(cls, field, field_name, lookup_expr=None): if lookup_expr is None: lookup_expr = settings.DEFAULT_LOOKUP_EXPR field, lookup_type = resolve_field(field, lookup_expr) default = { "field_name": field_name, "lookup_expr": lookup_expr, } filter_class, params = cls.filter_for_lookup(field, lookup_type) default.update(params) assert filter_class is not None, ( "%s resolved field '%s' with '%s' lookup to an unrecognized field " "type %s. Try adding an override to 'Meta.filter_overrides'. See: " "https://django-filter.readthedocs.io/en/main/ref/filterset.html" "#customise-filter-generation-with-filter-overrides" ) % (cls.__name__, field_name, lookup_expr, field.__class__.__name__) return filter_class(**default) @classmethod def filter_for_lookup(cls, field, lookup_type): DEFAULTS = dict(cls.FILTER_DEFAULTS) if hasattr(cls, "_meta"): DEFAULTS.update(cls._meta.filter_overrides) data = try_dbfield(DEFAULTS.get, field.__class__) or {} filter_class = data.get("filter_class") params = data.get("extra", lambda field: {})(field) # if there is no filter class, exit early if not filter_class: return None, {} # perform lookup specific checks if lookup_type == "exact" and getattr(field, "choices", None): return ChoiceFilter, {"choices": field.choices} if lookup_type == "isnull": data = try_dbfield(DEFAULTS.get, models.BooleanField) filter_class = data.get("filter_class") params = data.get("extra", lambda field: {})(field) return filter_class, params if lookup_type == "in": class ConcreteInFilter(BaseInFilter, filter_class): pass ConcreteInFilter.__name__ = cls._csv_filter_class_name( filter_class, lookup_type ) return ConcreteInFilter, params if lookup_type == "range": class ConcreteRangeFilter(BaseRangeFilter, filter_class): pass ConcreteRangeFilter.__name__ = cls._csv_filter_class_name( filter_class, lookup_type ) return ConcreteRangeFilter, params return filter_class, params @classmethod def _csv_filter_class_name(cls, filter_class, lookup_type): """ Generate a suitable class name for a concrete filter class. This is not completely reliable, as not all filter class names are of the format Filter. ex:: FilterSet._csv_filter_class_name(DateTimeFilter, 'in') returns 'DateTimeInFilter' """ # DateTimeFilter => DateTime type_name = filter_class.__name__ if type_name.endswith("Filter"): type_name = type_name[:-6] # in => In lookup_name = lookup_type.capitalize() # DateTimeInFilter return str("%s%sFilter" % (type_name, lookup_name)) class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass): pass def filterset_factory(model, fields=ALL_FIELDS): meta = type(str("Meta"), (object,), {"model": model, "fields": fields}) filterset = type( str("%sFilterSet" % model._meta.object_name), (FilterSet,), {"Meta": meta} ) return filterset django-filter-23.5/django_filters/constants.py0000644000175000017500000000007714533556010021436 0ustar carstencarstenALL_FIELDS = "__all__" EMPTY_VALUES = ([], (), {}, "", None) django-filter-23.5/django_filters/compat.py0000644000175000017500000000130714533556010020702 0ustar carstencarstenimport django from django.conf import settings from django.test import TestCase if django.VERSION < (4, 2): class TestCase(TestCase): assertQuerySetEqual = TestCase.assertQuerysetEqual # django-crispy-forms is optional try: 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 django-filter-23.5/django_filters/exceptions.py0000644000175000017500000000037514533556010021604 0ustar carstencarstenfrom 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) ) django-filter-23.5/django_filters/fields.py0000644000175000017500000002410214533556010020663 0ustar carstencarstenfrom 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) else: super()._set_choices(value) value = self.iterator(self, self._choices) # Simple `super()` syntax for calling a parent property setter is # unsupported. See https://github.com/python/cpython/issues/59170 super(ChoiceIteratorMixin, self.__class__).choices.__set__(self, value) # Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label 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 django-filter-23.5/django_filters/__init__.py0000644000175000017500000000143414533556010021157 0ustar carstencarsten# 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__ = "23.5" 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 django-filter-23.5/django_filters/views.py0000644000175000017500000000770214533556010020561 0ustar carstencarstenfrom 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) django-filter-23.5/django_filters/filters.py0000644000175000017500000006134714533556010021101 0ustar carstencarstenfrom collections import OrderedDict 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.itercompat import is_iterable from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from .conf import settings from .constants import EMPTY_VALUES from .fields import ( BaseCSVField, BaseRangeField, ChoiceField, DateRangeField, DateTimeRangeField, IsoDateTimeField, IsoDateTimeRangeField, LookupChoiceField, ModelChoiceField, ModelMultipleChoiceField, MultipleChoiceField, RangeField, TimeRangeField, ) from .utils import get_model_field, label_for_filter __all__ = [ "AllValuesFilter", "AllValuesMultipleFilter", "BaseCSVFilter", "BaseInFilter", "BaseRangeFilter", "BooleanFilter", "CharFilter", "ChoiceFilter", "DateFilter", "DateFromToRangeFilter", "DateRangeFilter", "DateTimeFilter", "DateTimeFromToRangeFilter", "DurationFilter", "Filter", "IsoDateTimeFilter", "IsoDateTimeFromToRangeFilter", "LookupChoiceFilter", "ModelChoiceFilter", "ModelMultipleChoiceFilter", "MultipleChoiceFilter", "NumberFilter", "NumericRangeFilter", "OrderingFilter", "RangeFilter", "TimeFilter", "TimeRangeFilter", "TypedChoiceFilter", "TypedMultipleChoiceFilter", "UUIDFilter", ] class Filter: creation_counter = 0 field_class = forms.Field def __init__( self, field_name=None, lookup_expr=None, *, label=None, method=None, distinct=False, exclude=False, **kwargs ): if lookup_expr is None: lookup_expr = settings.DEFAULT_LOOKUP_EXPR self.field_name = field_name self.lookup_expr = lookup_expr self.label = label self.method = method self.distinct = distinct self.exclude = exclude self.extra = kwargs self.extra.setdefault("required", False) self.creation_counter = Filter.creation_counter Filter.creation_counter += 1 def get_method(self, qs): """Return filter method based on whether we're excluding or simply filtering. """ return qs.exclude if self.exclude else qs.filter def method(): """ Filter method needs to be lazily resolved, as it may be dependent on the 'parent' FilterSet. """ def fget(self): return self._method def fset(self, value): self._method = value # clear existing FilterMethod if isinstance(self.filter, FilterMethod): del self.filter # override filter w/ FilterMethod. if value is not None: self.filter = FilterMethod(self) return locals() method = property(**method()) def label(): def fget(self): if self._label is None and hasattr(self, "model"): self._label = label_for_filter( self.model, self.field_name, self.lookup_expr, self.exclude ) return self._label def fset(self, value): self._label = value return locals() label = property(**label()) @property def field(self): if not hasattr(self, "_field"): field_kwargs = self.extra.copy() if settings.DISABLE_HELP_TEXT: field_kwargs.pop("help_text", None) self._field = self.field_class(label=self.label, **field_kwargs) return self._field def filter(self, qs, value): if value in EMPTY_VALUES: return qs if self.distinct: qs = qs.distinct() lookup = "%s__%s" % (self.field_name, self.lookup_expr) qs = self.get_method(qs)(**{lookup: value}) return qs class CharFilter(Filter): field_class = forms.CharField class BooleanFilter(Filter): field_class = forms.NullBooleanField class ChoiceFilter(Filter): field_class = ChoiceField def __init__(self, *args, **kwargs): self.null_value = kwargs.get("null_value", settings.NULL_CHOICE_VALUE) super().__init__(*args, **kwargs) def filter(self, qs, value): if value != self.null_value: return super().filter(qs, value) qs = self.get_method(qs)( **{"%s__%s" % (self.field_name, self.lookup_expr): None} ) return qs.distinct() if self.distinct else qs class TypedChoiceFilter(Filter): field_class = forms.TypedChoiceField class UUIDFilter(Filter): field_class = forms.UUIDField class MultipleChoiceFilter(Filter): """ This filter performs OR(by default) or AND(using conjoined=True) query on the selected options. Advanced usage -------------- Depending on your application logic, when all or no choices are selected, filtering may be a no-operation. In this case you may wish to avoid the filtering overhead, particularly if using a `distinct` call. You can override `get_filter_predicate` to use a custom filter. By default it will use the filter's name for the key, and the value will be the model object - or in case of passing in `to_field_name` the value of that attribute on the model. Set `always_filter` to `False` after instantiation to enable the default `is_noop` test. You can override `is_noop` if you need a different test for your application. `distinct` defaults to `True` as to-many relationships will generally require this. """ field_class = MultipleChoiceField always_filter = True def __init__(self, *args, **kwargs): kwargs.setdefault("distinct", True) self.conjoined = kwargs.pop("conjoined", False) self.null_value = kwargs.get("null_value", settings.NULL_CHOICE_VALUE) super().__init__(*args, **kwargs) def is_noop(self, qs, value): """ Return `True` to short-circuit unnecessary and potentially slow filtering. """ if self.always_filter: return False # A reasonable default for being a noop... if self.extra.get("required") and len(value) == len(self.field.choices): return True return False def filter(self, qs, value): if not value: # Even though not a noop, no point filtering if empty. return qs if self.is_noop(qs, value): return qs if not self.conjoined: q = Q() for v in set(value): if v == self.null_value: v = None predicate = self.get_filter_predicate(v) if self.conjoined: qs = self.get_method(qs)(**predicate) else: q |= Q(**predicate) if not self.conjoined: qs = self.get_method(qs)(q) return qs.distinct() if self.distinct else qs def get_filter_predicate(self, v): name = self.field_name if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR: name = LOOKUP_SEP.join([name, self.lookup_expr]) try: return {name: getattr(v, self.field.to_field_name)} except (AttributeError, TypeError): return {name: v} class TypedMultipleChoiceFilter(MultipleChoiceFilter): field_class = forms.TypedMultipleChoiceField class DateFilter(Filter): field_class = forms.DateField class DateTimeFilter(Filter): field_class = forms.DateTimeField class IsoDateTimeFilter(DateTimeFilter): """ Uses IsoDateTimeField to support filtering on ISO 8601 formatted datetimes. For context see: * https://code.djangoproject.com/ticket/23448 * https://github.com/encode/django-rest-framework/issues/1338 * https://github.com/carltongibson/django-filter/pull/264 """ field_class = IsoDateTimeField class TimeFilter(Filter): field_class = forms.TimeField class DurationFilter(Filter): field_class = forms.DurationField class QuerySetRequestMixin: """ Add callable functionality to filters that support the ``queryset`` argument. If the ``queryset`` is callable, then it **must** accept the ``request`` object as a single argument. This is useful for filtering querysets by properties on the ``request`` object, such as the user. Example:: def departments(request): company = request.user.company return company.department_set.all() class EmployeeFilter(filters.FilterSet): department = filters.ModelChoiceFilter(queryset=departments) ... The above example restricts the set of departments to those in the logged-in user's associated company. """ def __init__(self, *args, **kwargs): self.queryset = kwargs.get("queryset") super().__init__(*args, **kwargs) def get_request(self): try: return self.parent.request except AttributeError: return None def get_queryset(self, request): queryset = self.queryset if callable(queryset): return queryset(request) return queryset @property def field(self): request = self.get_request() queryset = self.get_queryset(request) if queryset is not None: self.extra["queryset"] = queryset return super().field class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter): field_class = ModelChoiceField def __init__(self, *args, **kwargs): kwargs.setdefault("empty_label", settings.EMPTY_CHOICE_LABEL) super().__init__(*args, **kwargs) class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter): field_class = ModelMultipleChoiceField class NumberFilter(Filter): field_class = forms.DecimalField def get_max_validator(self): """ Return a MaxValueValidator for the field, or None to disable. """ return MaxValueValidator(1e50) @property def field(self): if not hasattr(self, "_field"): field = super().field max_validator = self.get_max_validator() if max_validator: field.validators.append(max_validator) self._field = field return self._field class NumericRangeFilter(Filter): field_class = RangeField def filter(self, qs, value): if value: if value.start is not None and value.stop is not None: value = (value.start, value.stop) elif value.start is not None: self.lookup_expr = "startswith" value = value.start elif value.stop is not None: self.lookup_expr = "endswith" value = value.stop return super().filter(qs, value) class RangeFilter(Filter): field_class = RangeField def filter(self, qs, value): if value: if value.start is not None and value.stop is not None: self.lookup_expr = "range" value = (value.start, value.stop) elif value.start is not None: self.lookup_expr = "gte" value = value.start elif value.stop is not None: self.lookup_expr = "lte" value = value.stop return super().filter(qs, value) def _truncate(dt): return dt.date() class DateRangeFilter(ChoiceFilter): choices = [ ("today", _("Today")), ("yesterday", _("Yesterday")), ("week", _("Past 7 days")), ("month", _("This month")), ("year", _("This year")), ] filters = { "today": lambda qs, name: qs.filter( **{ "%s__year" % name: now().year, "%s__month" % name: now().month, "%s__day" % name: now().day, } ), "yesterday": lambda qs, name: qs.filter( **{ "%s__year" % name: (now() - timedelta(days=1)).year, "%s__month" % name: (now() - timedelta(days=1)).month, "%s__day" % name: (now() - timedelta(days=1)).day, } ), "week": lambda qs, name: qs.filter( **{ "%s__gte" % name: _truncate(now() - timedelta(days=7)), "%s__lt" % name: _truncate(now() + timedelta(days=1)), } ), "month": lambda qs, name: qs.filter( **{"%s__year" % name: now().year, "%s__month" % name: now().month} ), "year": lambda qs, name: qs.filter( **{ "%s__year" % name: now().year, } ), } def __init__(self, choices=None, filters=None, *args, **kwargs): if choices is not None: self.choices = choices if filters is not None: self.filters = filters all_choices = list( chain.from_iterable( [subchoice[0] for subchoice in choice[1]] if isinstance(choice[1], (list, tuple)) # This is an optgroup else [choice[0]] for choice in self.choices ) ) unique = set(all_choices) ^ set(self.filters) assert not unique, ( "Keys must be present in both 'choices' and 'filters'. Missing keys: " "'%s'" % ", ".join(sorted(unique)) ) # null choice not relevant kwargs.setdefault("null_label", None) super().__init__(choices=self.choices, *args, **kwargs) def filter(self, qs, value): if not value: return qs assert value in self.filters qs = self.filters[value](qs, self.field_name) return qs.distinct() if self.distinct else qs class DateFromToRangeFilter(RangeFilter): field_class = DateRangeField class DateTimeFromToRangeFilter(RangeFilter): field_class = DateTimeRangeField class IsoDateTimeFromToRangeFilter(RangeFilter): field_class = IsoDateTimeRangeField class TimeRangeFilter(RangeFilter): field_class = TimeRangeField class AllValuesFilter(ChoiceFilter): @property def field(self): qs = self.model._default_manager.distinct() qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True) self.extra["choices"] = [(o, o) for o in qs] return super().field class AllValuesMultipleFilter(MultipleChoiceFilter): @property def field(self): qs = self.model._default_manager.distinct() qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True) self.extra["choices"] = [(o, o) for o in qs] return super().field class BaseCSVFilter(Filter): """ Base class for CSV type filters, such as IN and RANGE. """ base_field_class = BaseCSVField def __init__(self, *args, **kwargs): kwargs.setdefault("help_text", _("Multiple values may be separated by commas.")) super().__init__(*args, **kwargs) class ConcreteCSVField(self.base_field_class, self.field_class): pass ConcreteCSVField.__name__ = self._field_class_name( self.field_class, self.lookup_expr ) self.field_class = ConcreteCSVField @classmethod def _field_class_name(cls, field_class, lookup_expr): """ Generate a suitable class name for the concrete field class. This is not completely reliable, as not all field class names are of the format Field. ex:: BaseCSVFilter._field_class_name(DateTimeField, 'year__in') returns 'DateTimeYearInField' """ # DateTimeField => DateTime type_name = field_class.__name__ if type_name.endswith("Field"): type_name = type_name[:-5] # year__in => YearIn parts = lookup_expr.split(LOOKUP_SEP) expression_name = "".join(p.capitalize() for p in parts) # DateTimeYearInField return str("%s%sField" % (type_name, expression_name)) class BaseInFilter(BaseCSVFilter): def __init__(self, *args, **kwargs): kwargs.setdefault("lookup_expr", "in") super().__init__(*args, **kwargs) class BaseRangeFilter(BaseCSVFilter): base_field_class = BaseRangeField def __init__(self, *args, **kwargs): kwargs.setdefault("lookup_expr", "range") super().__init__(*args, **kwargs) class LookupChoiceFilter(Filter): """ A combined filter that allows users to select the lookup expression from a dropdown. * ``lookup_choices`` is an optional argument that accepts multiple input formats, and is ultimately normalized as the choices used in the lookup dropdown. See ``.get_lookup_choices()`` for more information. * ``field_class`` is an optional argument that allows you to set the inner form field class used to validate the value. Default: ``forms.CharField`` ex:: price = django_filters.LookupChoiceFilter( field_class=forms.DecimalField, lookup_choices=[ ('exact', 'Equals'), ('gt', 'Greater than'), ('lt', 'Less than'), ] ) """ field_class = forms.CharField outer_class = LookupChoiceField def __init__( self, field_name=None, lookup_choices=None, field_class=None, **kwargs ): self.empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL) super(LookupChoiceFilter, self).__init__(field_name=field_name, **kwargs) self.lookup_choices = lookup_choices if field_class is not None: self.field_class = field_class @classmethod def normalize_lookup(cls, lookup): """ Normalize the lookup into a tuple of ``(lookup expression, display value)`` If the ``lookup`` is already a tuple, the tuple is not altered. If the ``lookup`` is a string, a tuple is returned with the lookup expression used as the basis for the display value. ex:: >>> LookupChoiceFilter.normalize_lookup(('exact', 'Equals')) ('exact', 'Equals') >>> LookupChoiceFilter.normalize_lookup('has_key') ('has_key', 'Has key') """ if isinstance(lookup, str): return (lookup, pretty_name(lookup)) return (lookup[0], lookup[1]) def get_lookup_choices(self): """ Get the lookup choices in a format suitable for ``django.forms.ChoiceField``. If the filter is initialized with ``lookup_choices``, this value is normalized and passed to the underlying ``LookupChoiceField``. If no choices are provided, they are generated from the corresponding model field's registered lookups. """ lookups = self.lookup_choices if lookups is None: field = get_model_field(self.model, self.field_name) lookups = field.get_lookups() return [self.normalize_lookup(lookup) for lookup in lookups] @property def field(self): if not hasattr(self, "_field"): inner_field = super().field lookups = self.get_lookup_choices() self._field = self.outer_class( inner_field, lookups, label=self.label, empty_label=self.empty_label, required=self.extra["required"], ) return self._field def filter(self, qs, lookup): if not lookup: return super().filter(qs, None) self.lookup_expr = lookup.lookup_expr return super().filter(qs, lookup.value) class OrderingFilter(BaseCSVFilter, ChoiceFilter): """ Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts two additional arguments that are used to build the ordering choices. * ``fields`` is a mapping of {model field name: parameter name}. The parameter names are exposed in the choices and mask/alias the field names used in the ``order_by()`` call. Similar to field ``choices``, ``fields`` accepts the 'list of two-tuples' syntax that retains order. ``fields`` may also just be an iterable of strings. In this case, the field names simply double as the exposed parameter names. * ``field_labels`` is an optional argument that allows you to customize the display label for the corresponding parameter. It accepts a mapping of {field name: human readable label}. Keep in mind that the key is the field name, and not the exposed parameter name. Additionally, you can just provide your own ``choices`` if you require explicit control over the exposed options. For example, when you might want to disable descending sort options. This filter is also CSV-based, and accepts multiple ordering params. The default select widget does not enable the use of this, but it is useful for APIs. """ descending_fmt = _("%s (descending)") def __init__(self, *args, **kwargs): """ ``fields`` may be either a mapping or an iterable. ``field_labels`` must be a map of field names to display labels """ fields = kwargs.pop("fields", {}) fields = self.normalize_fields(fields) field_labels = kwargs.pop("field_labels", {}) self.param_map = {v: k for k, v in fields.items()} if "choices" not in kwargs: kwargs["choices"] = self.build_choices(fields, field_labels) kwargs.setdefault("label", _("Ordering")) kwargs.setdefault("help_text", "") kwargs.setdefault("null_label", None) super().__init__(*args, **kwargs) def get_ordering_value(self, param): descending = param.startswith("-") param = param[1:] if descending else param field_name = self.param_map.get(param, param) return "-%s" % field_name if descending else field_name def filter(self, qs, value): if value in EMPTY_VALUES: return qs ordering = [ self.get_ordering_value(param) for param in value if param not in EMPTY_VALUES ] return qs.order_by(*ordering) @classmethod def normalize_fields(cls, fields): """ Normalize the fields into an ordered map of {field name: param name} """ # fields is a mapping, copy into new OrderedDict if isinstance(fields, dict): return OrderedDict(fields) # convert iterable of values => iterable of pairs (field name, param name) assert is_iterable( fields ), "'fields' must be an iterable (e.g., a list, tuple, or mapping)." # fields is an iterable of field names assert all( isinstance(field, str) or is_iterable(field) and len(field) == 2 # may need to be wrapped in parens for field in fields ), "'fields' must contain strings or (field name, param name) pairs." 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 django-filter-23.5/django_filters/templates/0000755000175000017500000000000014533556010021042 5ustar carstencarstendjango-filter-23.5/django_filters/templates/django_filters/0000755000175000017500000000000014533556010024034 5ustar carstencarstendjango-filter-23.5/django_filters/templates/django_filters/widgets/0000755000175000017500000000000014533556010025502 5ustar carstencarstendjango-filter-23.5/django_filters/templates/django_filters/widgets/multiwidget.html0000644000175000017500000000016614533556010030731 0ustar carstencarsten{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %} django-filter-23.5/django_filters/templates/django_filters/rest_framework/0000755000175000017500000000000014533556010027066 5ustar carstencarstendjango-filter-23.5/django_filters/templates/django_filters/rest_framework/crispy_form.html0000644000175000017500000000015414533556010032310 0ustar carstencarsten{% load crispy_forms_tags %} {% load i18n %}

{% trans "Field filters" %}

{% crispy filter.form %} django-filter-23.5/django_filters/templates/django_filters/rest_framework/form.html0000644000175000017500000000032314533556010030715 0ustar carstencarsten{% load i18n %}

{% trans "Field filters" %}

{{ filter.form.as_p }}
django-filter-23.5/django_filters/rest_framework/0000755000175000017500000000000014533556010022076 5ustar carstencarstendjango-filter-23.5/django_filters/rest_framework/filterset.py0000644000175000017500000000222614533556010024453 0ustar carstencarstenfrom 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 django-filter-23.5/django_filters/rest_framework/__init__.py0000644000175000017500000000016114533556010024205 0ustar carstencarsten# flake8: noqa from .backends import DjangoFilterBackend from .filters import * from .filterset import FilterSet django-filter-23.5/django_filters/rest_framework/filters.py0000644000175000017500000000047014533556010024121 0ustar carstencarstenfrom 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) django-filter-23.5/django_filters/rest_framework/backends.py0000644000175000017500000001316014533556010024223 0ustar carstencarstenimport warnings from django.template import loader from .. import compat, utils from . import filters, filterset class DjangoFilterBackend: filterset_base = filterset.FilterSet raise_exception = True @property def template(self): if compat.is_crispy(): return "django_filters/rest_framework/crispy_form.html" return "django_filters/rest_framework/form.html" def get_filterset(self, request, queryset, view): filterset_class = self.get_filterset_class(view, queryset) if filterset_class is None: return None kwargs = self.get_filterset_kwargs(request, queryset, view) return filterset_class(**kwargs) def get_filterset_class(self, view, queryset=None): """ Return the `FilterSet` class used to filter the queryset. """ filterset_class = getattr(view, "filterset_class", None) filterset_fields = getattr(view, "filterset_fields", None) if filterset_class: filterset_model = filterset_class._meta.model # FilterSets do not need to specify a Meta class if filterset_model and queryset is not None: assert issubclass( queryset.model, filterset_model ), "FilterSet model %s does not match queryset model %s" % ( filterset_model, queryset.model, ) return filterset_class if filterset_fields and queryset is not None: MetaBase = getattr(self.filterset_base, "Meta", object) class AutoFilterSet(self.filterset_base): class Meta(MetaBase): model = queryset.model fields = filterset_fields return AutoFilterSet return None def get_filterset_kwargs(self, request, queryset, view): return { "data": request.query_params, "queryset": queryset, "request": request, } def filter_queryset(self, request, queryset, view): filterset = self.get_filterset(request, queryset, view) if filterset is None: return queryset if not filterset.is_valid() and self.raise_exception: raise utils.translate_validation(filterset.errors) return filterset.qs def to_html(self, request, queryset, view): filterset = self.get_filterset(request, queryset, view) if filterset is None: return None template = loader.get_template(self.template) context = {"filter": filterset} return template.render(context, request) def get_coreschema_field(self, field): if isinstance(field, filters.NumberFilter): field_cls = compat.coreschema.Number else: field_cls = compat.coreschema.String return field_cls(description=str(field.extra.get("help_text", ""))) def get_schema_fields(self, view): # This is not compatible with widgets where the query param differs from the # filter's attribute name. Notably, this includes `MultiWidget`, where query # params will be of the format `_0`, `_1`, etc... from django_filters import RemovedInDjangoFilter25Warning warnings.warn( "Built-in schema generation is deprecated. Use drf-spectacular.", category=RemovedInDjangoFilter25Warning, ) assert ( compat.coreapi is not None ), "coreapi must be installed to use `get_schema_fields()`" assert ( compat.coreschema is not None ), "coreschema must be installed to use `get_schema_fields()`" try: queryset = view.get_queryset() except Exception: queryset = None warnings.warn( "{} is not compatible with schema generation".format(view.__class__) ) filterset_class = self.get_filterset_class(view, queryset) return ( [] if not filterset_class else [ compat.coreapi.Field( name=field_name, required=field.extra["required"], location="query", schema=self.get_coreschema_field(field), ) for field_name, field in filterset_class.base_filters.items() ] ) def get_schema_operation_parameters(self, view): from django_filters import RemovedInDjangoFilter25Warning warnings.warn( "Built-in schema generation is deprecated. Use drf-spectacular.", category=RemovedInDjangoFilter25Warning, ) try: queryset = view.get_queryset() except Exception: queryset = None warnings.warn( "{} is not compatible with schema generation".format(view.__class__) ) filterset_class = self.get_filterset_class(view, queryset) if not filterset_class: return [] parameters = [] for field_name, field in filterset_class.base_filters.items(): parameter = { "name": field_name, "required": field.extra["required"], "in": "query", "description": field.label if field.label is not None else field_name, "schema": { "type": "string", }, } if field.extra and "choices" in field.extra: parameter["schema"]["enum"] = [c[0] for c in field.extra["choices"]] parameters.append(parameter) return parameters django-filter-23.5/django_filters/widgets.py0000644000175000017500000002147514533556010021075 0ustar carstencarstenfrom collections.abc import Iterable from copy import deepcopy from itertools import chain from re import search, sub from django import forms from django.db.models.fields import BLANK_CHOICE_DASH from django.forms.utils import flatatt from django.utils.datastructures import MultiValueDict from django.utils.encoding import force_str from django.utils.http import urlencode from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ class LinkWidget(forms.Widget): def __init__(self, attrs=None, choices=()): super().__init__(attrs) self.choices = choices def value_from_datadict(self, data, files, name): value = super().value_from_datadict(data, files, name) self.data = data return value def render(self, name, value, attrs=None, choices=(), renderer=None): if not hasattr(self, "data"): self.data = {} if value is None: value = "" final_attrs = self.build_attrs(self.attrs, extra_attrs=attrs) output = ["" % flatatt(final_attrs)] options = self.render_options(choices, [value], name) if options: output.append(options) output.append("") return mark_safe("\n".join(output)) def render_options(self, choices, selected_choices, name): selected_choices = set(force_str(v) for v in selected_choices) output = [] for option_value, option_label in chain(self.choices, choices): if isinstance(option_label, (list, tuple)): for option in option_label: output.append(self.render_option(name, selected_choices, *option)) else: output.append( self.render_option( name, selected_choices, option_value, option_label ) ) return "\n".join(output) def render_option(self, name, selected_choices, option_value, option_label): option_value = force_str(option_value) if option_label == BLANK_CHOICE_DASH[0][1]: option_label = _("All") data = self.data.copy() data[name] = option_value selected = data == self.data or option_value in selected_choices try: url = data.urlencode() except AttributeError: url = urlencode(data) return self.option_string() % { "attrs": selected and ' class="selected"' or "", "query_string": url, "label": force_str(option_label), } def option_string(self): return '
  • %(label)s
  • ' class SuffixedMultiWidget(forms.MultiWidget): """ A MultiWidget that allows users to provide custom suffixes instead of indexes. - Suffixes must be unique. - There must be the same number of suffixes as fields. """ suffixes = [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) assert len(self.widgets) == len(self.suffixes) assert len(self.suffixes) == len(set(self.suffixes)) def suffixed(self, name, suffix): return "_".join([name, suffix]) if suffix else name def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) for subcontext, suffix in zip(context["widget"]["subwidgets"], self.suffixes): subcontext["name"] = self.suffixed(name, suffix) return context def value_from_datadict(self, data, files, name): return [ widget.value_from_datadict(data, files, self.suffixed(name, suffix)) for widget, suffix in zip(self.widgets, self.suffixes) ] def value_omitted_from_data(self, data, files, name): return all( widget.value_omitted_from_data(data, files, self.suffixed(name, suffix)) for widget, suffix in zip(self.widgets, self.suffixes) ) def replace_name(self, output, index): result = search(r'name="(?P.*)_%d"' % index, output) name = result.group("name") name = self.suffixed(name, self.suffixes[index]) name = 'name="%s"' % name return sub(r'name=".*_%d"' % index, name, output) def decompress(self, value): if value is None: return [None, None] return value class RangeWidget(SuffixedMultiWidget): template_name = "django_filters/widgets/multiwidget.html" suffixes = ["min", "max"] def __init__(self, attrs=None): widgets = (forms.TextInput, forms.TextInput) super().__init__(widgets, attrs) def decompress(self, value): if value: return [value.start, value.stop] return [None, None] class DateRangeWidget(RangeWidget): suffixes = ["after", "before"] class LookupChoiceWidget(SuffixedMultiWidget): suffixes = [None, "lookup"] def decompress(self, value): if value is None: return [None, None] return value class BooleanWidget(forms.Select): """Convert true/false values into the internal Python True/False. This can be used for AJAX queries that pass true/false from JavaScript's internal types through. """ def __init__(self, attrs=None): choices = (("", _("Unknown")), ("true", _("Yes")), ("false", _("No"))) super().__init__(attrs, choices) def render(self, name, value, attrs=None, renderer=None): try: value = {True: "true", False: "false", "1": "true", "0": "false"}[value] except KeyError: value = "" return super().render(name, value, attrs, renderer=renderer) def value_from_datadict(self, data, files, name): value = data.get(name, None) if isinstance(value, str): value = value.lower() return { "1": True, "0": False, "true": True, "false": False, True: True, False: False, }.get(value, None) class BaseCSVWidget(forms.Widget): # Surrogate widget for rendering multiple values surrogate = forms.TextInput def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if isinstance(self.surrogate, type): self.surrogate = self.surrogate() else: self.surrogate = deepcopy(self.surrogate) def _isiterable(self, value): return isinstance(value, Iterable) and not isinstance(value, str) def value_from_datadict(self, data, files, name): value = super().value_from_datadict(data, files, name) if value is not None: if value == "": # empty value should parse as an empty list return [] 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)) django-filter-23.5/django_filters/utils.py0000644000175000017500000002635014533556010020564 0ustar carstencarstenimport 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): # Pre-4.x versions of Django have is_dst. Later Django versions have # zoneinfo where the is_dst argument has no meaning. is_dst will be # removed in the 5.x series. # # On intermediate versions, the default is to use zoneinfo, but pytz # is still available under USE_DEPRECATED_PYTZ, and is_dst is # meaningful there. Under those versions we should only use is_dst # if USE_DEPRECATED_PYTZ is present and True; otherwise, we will cause # deprecation warnings, and we should not. See #1580. # # This can be removed once 3.2 is no longer supported upstream. if django.VERSION < (4, 0) or (django.VERSION < (5, 0) and settings.USE_DEPRECATED_PYTZ): return timezone.make_aware(value, timezone.get_current_timezone(), is_dst) return timezone.make_aware(value, timezone.get_current_timezone()) elif not settings.USE_TZ and timezone.is_aware(value): return timezone.make_naive(value, 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) django-filter-23.5/docs/0000755000175000017500000000000014533556010015002 5ustar carstencarstendjango-filter-23.5/docs/dev/0000755000175000017500000000000014533556010015560 5ustar carstencarstendjango-filter-23.5/docs/dev/tests.txt0000644000175000017500000000362714533556010017473 0ustar carstencarsten====================== Running the Test Suite ====================== The easiest way to run the django-filter tests is to check out the source code and create a virtualenv where you can install the test dependencies. Django-filter uses a custom test runner to configure the environment, so a wrapper script is available to set up and run the test suite. .. note:: The following assumes you have `virtualenv`__ and `git`__ installed. __ https://virtualenv.pypa.io/en/stable/ __ https://git-scm.com Clone the repository -------------------- Get the source code using the following command: .. code-block:: bash $ git clone https://github.com/carltongibson/django-filter.git Switch to the django-filter directory: .. code-block:: bash $ cd django-filter Set up the virtualenv --------------------- Create a new virtualenv to run the test suite in: .. code-block:: bash $ virtualenv venv Then activate the virtualenv and install the test requirements: .. code-block:: bash $ source venv/bin/activate $ pip install -r requirements/test.txt Execute the test runner ----------------------- Run the tests with the runner script: .. code-block:: bash $ python runtests.py Test all supported versions --------------------------- You can also use the excellent tox testing tool to run the tests against all supported versions of Python and Django. Install tox, and then simply run: .. code-block:: bash $ pip install tox $ tox Housekeeping ------------ The ``isort`` utility is used to maintain module imports. You can either test the module imports with the appropriate `tox` env, or with `isort` directly. .. code-block:: bash $ pip install tox $ tox -e isort # or $ pip install isort $ isort --check --diff django_filters tests To sort the imports, simply remove the ``--check-only`` option. .. code-block:: bash $ isort --recursive django_filters tests django-filter-23.5/docs/guide/0000755000175000017500000000000014533556010016077 5ustar carstencarstendjango-filter-23.5/docs/guide/migration.txt0000644000175000017500000002570314533556010020640 0ustar carstencarsten=============== Migration Guide =============== ----------------- Enabling warnings ----------------- To view deprecations, you may need to enable warnings within Python. This can be achieved with either the ``-W`` `flag`__, or with ``PYTHONWARNINGS`` `environment variable`__. For example, you could run your test suite like so: .. code-block:: bash $ python -W once manage.py test The above would print all warnings once when they first occur. This is useful to know what violations exist in your code (or occasionally in third party code). However, it only prints the last line of the stack trace. You can use the following to raise the full exception instead: .. code-block:: bash $ python -W error manage.py test __ https://docs.python.org/3.6/using/cmdline.html#cmdoption-W __ https://docs.python.org/3.6/using/cmdline.html#envvar-PYTHONWARNINGS ---------------- Migrating to 2.0 ---------------- This release contains several changes that break forwards compatibility. This includes removed features, renamed attributes and arguments, and some reworked features. Due to the nature of these changes, it is not feasible to release a fully forwards-compatible migration release. Please review the below list of changes and update your code accordingly. ``Filter.lookup_expr`` list form removed (`#851`__) --------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/851 The ``Filter.lookup_expr`` argument no longer accepts ``None`` or a list of expressions. Use the :ref:`LookupChoiceFilter ` instead. FilterSet ``filter_for_reverse_field`` removed (`#915`__) --------------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/915 The ``filter_for_field`` method now generates filters for reverse relationships, removing the need for ``filter_for_reverse_field``. As a result, reverse relationships now also obey ``Meta.filter_overrides``. View attributes renamed (`#867`__) ---------------------------------- __ https://github.com/carltongibson/django-filter/pull/867 Several view-related attributes have been renamed to improve consistency with other parts of the library. The following classes are affected: * DRF ``ViewSet.filter_class`` => ``filterset_class`` * DRF ``ViewSet.filter_fields`` => ``filterset_fields`` * ``DjangoFilterBackend.default_filter_set`` => ``filterset_base`` * ``DjangoFilterBackend.get_filter_class()`` => ``get_filterset_class()`` * ``FilterMixin.filter_fields`` => ``filterset_fields`` FilterSet ``Meta.together`` option removed (`#791`__) ----------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/791 The ``Meta.together`` has been deprecated in favor of userland implementations that override the ``clean`` method of the ``Meta.form`` class. An example will be provided in a "recipes" section in future docs. FilterSet "strictness" handling moved to view (`#788`__) -------------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/788 Strictness handling has been removed from the ``FilterSet`` and added to the view layer. As a result, the ``FILTERS_STRICTNESS`` setting, ``Meta.strict`` option, and ``strict`` argument for the ``FilterSet`` initializer have all been removed. To alter strictness behavior, the appropriate view code should be overridden. More details will be provided in future docs. ``Filter.name`` renamed to ``Filter.field_name`` (`#792`__) ----------------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/792 The filter ``name`` has been renamed to ``field_name`` as a way to disambiguate the filter's attribute name on its FilterSet class from the ``field_name`` used for filtering purposes. ``Filter.widget`` and ``Filter.required`` removed (`#734`__) ------------------------------------------------------------ __ https://github.com/carltongibson/django-filter/pull/734 The filter class no longer directly stores arguments passed to its form field. All arguments are located in the filter's ``.extra`` dict. ``MultiWidget`` replaced by ``SuffixedMultiWidget`` (`#770`__) -------------------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/770 ``RangeWidget``, ``DateRangeWidget``, and ``LookupTypeWidget`` now inherit from ``SuffixedMultiWidget``, changing the suffixes of their query param names. For example, ``RangeWidget`` now has ``_min`` and ``_max`` suffixes instead of ``_0`` and ``_1``. Filters like ``RangeFilter, DateRangeFilter, DateTimeFromToRangeFilter...`` (`#770`__) -------------------------------------------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/770 As they depend on ``MultiWidget``, they need to be adjusted. In 1.0 release parameters were provided using ``_0`` and ``_1`` as suffix``. For example, a parameter ``creation_date`` using``DateRangeFilter`` will expect ``creation_date_after`` and ``creation_date_before`` instead of ``creation_date_0`` and ``creation_date_1``. ---------------- Migrating to 1.0 ---------------- The 1.0 release of django-filter introduces several API changes and refinements that break forwards compatibility. Below is a list of deprecations and instructions on how to migrate to the 1.0 release. A forwards-compatible 0.15 release has also been created to help with migration. It is compatible with both the existing and new APIs and will raise warnings for deprecated behavior. MethodFilter and Filter.action replaced by Filter.method (`#382`__) ------------------------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/382 The functionality of ``MethodFilter`` and ``Filter.action`` has been merged together and replaced by the ``Filter.method`` parameter. The ``method`` parameter takes either a callable or the name of a ``FilterSet`` method. The signature now takes an additional ``name`` argument that is the name of the model field to be filtered on. Since ``method`` is now a parameter of all filters, inputs are validated and cleaned by its ``field_class``. The function will receive the cleaned value instead of the raw value. .. code-block:: python # 0.x class UserFilter(FilterSet): last_login = filters.MethodFilter() def filter_last_login(self, qs, value): # try to convert value to datetime, which may fail. if value and looks_like_a_date(value): value = datetime(value) return qs.filter(last_login=value}) # 1.0 class UserFilter(FilterSet): last_login = filters.CharFilter(method='filter_last_login') def filter_last_login(self, qs, name, value): return qs.filter(**{name: value}) QuerySet methods are no longer proxied (`#440`__) ------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/440 The ``__iter__()``, ``__len__()``, ``__getitem__()``, ``count()`` methods are no longer proxied from the queryset. To fix this, call the methods on the ``.qs`` property itself. .. code-block:: python f = UserFilter(request.GET, queryset=User.objects.all()) # 0.x for obj in f: ... # 1.0 for obj in f.qs: ... Filters no longer autogenerated when Meta.fields is not specified (`#450`__) ---------------------------------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/450 FilterSets had an undocumented behavior of autogenerating filters for all model fields when either ``Meta.fields`` was not specified or when set to ``None``. This can lead to potentially unsafe data or schema exposure and has been deprecated in favor of explicitly setting ``Meta.fields`` to the ``'__all__'`` special value. You may also blacklist fields by setting the ``Meta.exclude`` attribute. .. code-block:: python class UserFilter(FilterSet): class Meta: model = User fields = '__all__' # or class UserFilter(FilterSet): class Meta: model = User exclude = ['password'] Move FilterSet options to Meta class (`#430`__) ----------------------------------------------- __ https://github.com/carltongibson/django-filter/issues/430 Several ``FilterSet`` options have been moved to the ``Meta`` class to prevent potential conflicts with declared filter names. This includes: * ``filter_overrides`` * ``strict`` * ``order_by_field`` .. code-block:: python # 0.x class UserFilter(FilterSet): filter_overrides = {} strict = STRICTNESS.RAISE_VALIDATION_ERROR order_by_field = 'order' ... # 1.0 class UserFilter(FilterSet): ... class Meta: filter_overrides = {} strict = STRICTNESS.RAISE_VALIDATION_ERROR order_by_field = 'order' FilterSet ordering replaced by OrderingFilter (`#472`__) -------------------------------------------------------- __ https://github.com/carltongibson/django-filter/pull/472 The FilterSet ordering options and methods have been deprecated and replaced by :ref:`OrderingFilter `. Deprecated options include: * ``Meta.order_by`` * ``Meta.order_by_field`` These options retain backwards compatibility with the following caveats: * ``order_by`` asserts that ``Meta.fields`` is not using the dict syntax. This previously was undefined behavior, however the migration code is unable to support it. * Prior, if no ordering was specified in the request, the FilterSet implicitly filtered by the first param in the ``order_by`` option. This behavior cannot be easily emulated but can be fixed by ensuring that the passed in queryset explicitly calls ``.order_by()``. .. code-block:: python filterset = MyFilterSet(queryset=MyModel.objects.order_by('field')) The following methods are deprecated and will raise an assertion if present on the FilterSet: * ``.get_order_by()`` * ``.get_ordering_field()`` To fix this, simply remove the methods from your class. You can subclass ``OrderingFilter`` to migrate any custom logic. Deprecated ``FILTERS_HELP_TEXT_FILTER`` and ``FILTERS_HELP_TEXT_EXCLUDE`` (`#437`__) ------------------------------------------------------------------------------------ __ https://github.com/carltongibson/django-filter/pull/437 Generated filter labels in 1.0 will be more descriptive, including humanized text about the lookup being performed and if the filter is an exclusion filter. These settings will no longer have an effect and will be removed in the 1.0 release. DRF filter backend raises ``TemplateDoesNotExist`` exception (`#562`__) ----------------------------------------------------------------------- __ https://github.com/carltongibson/django-filter/issues/562 Templates are now provided by django-filter. If you are receiving this error, you may need to add ``'django_filters'`` to your ``INSTALLED_APPS`` setting. Alternatively, you could provide your own templates. django-filter-23.5/docs/guide/install.txt0000644000175000017500000000115614533556010020311 0ustar carstencarsten============ Installation ============ Django-filter can be installed from PyPI with tools like ``pip``: .. code-block:: bash $ pip install django-filter Then add ``'django_filters'`` to your ``INSTALLED_APPS``. .. code-block:: python INSTALLED_APPS = [ ... 'django_filters', ] Requirements ------------ Django-filter requires a current version of `Django`__ and is tested against all supported versions of Python, as well as the latest version of Django REST Framework (`DRF`__). __ https://www.djangoproject.com/download/#supported-versions __ http://www.django-rest-framework.org/ django-filter-23.5/docs/guide/usage.txt0000644000175000017500000002577514533556010017764 0ustar carstencarsten=============== Getting Started =============== Django-filter provides a simple way to filter down a queryset based on parameters a user provides. Say we have a ``Product`` model and we want to let our users filter which products they see on a list page. .. note:: If you're using django-filter with Django Rest Framework, it's recommended that you read the :ref:`drf integration` docs after this guide. The model --------- Let's start with our model:: from django.db import models class Product(models.Model): name = models.CharField(max_length=255) price = models.DecimalField(max_digits=5, decimal_places=2) description = models.TextField() release_date = models.DateField() manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE) The filter ---------- We have a number of fields and we want to let our users filter based on the name, the price or the release_date. We create a ``FilterSet`` for this:: import django_filters class ProductFilter(django_filters.FilterSet): name = django_filters.CharFilter(lookup_expr='iexact') class Meta: model = Product fields = ['price', 'release_date'] As you can see this uses a very similar API to Django's ``ModelForm``. Just like with a ``ModelForm`` we can also override filters, or add new ones using a declarative syntax. Declaring filters ~~~~~~~~~~~~~~~~~ The declarative syntax provides you with the most flexibility when creating filters, however it is fairly verbose. We'll use the below example to outline the :ref:`core filter arguments ` on a ``FilterSet``:: class ProductFilter(django_filters.FilterSet): price = django_filters.NumberFilter() price__gt = django_filters.NumberFilter(field_name='price', lookup_expr='gt') price__lt = django_filters.NumberFilter(field_name='price', lookup_expr='lt') release_year = django_filters.NumberFilter(field_name='release_date', lookup_expr='year') release_year__gt = django_filters.NumberFilter(field_name='release_date', lookup_expr='year__gt') release_year__lt = django_filters.NumberFilter(field_name='release_date', lookup_expr='year__lt') manufacturer__name = django_filters.CharFilter(lookup_expr='icontains') class Meta: model = Product fields = ['price', 'release_date', 'manufacturer'] There are two main arguments for filters: - ``field_name``: The name of the model field to filter on. You can traverse "relationship paths" using Django's ``__`` syntax to filter fields on a related model. ex, ``manufacturer__name``. - ``lookup_expr``: The `field lookup`_ to use when filtering. Django's ``__`` syntax can again be used in order to support lookup transforms. ex, ``year__gte``. .. _`field lookup`: https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups Together, the field ``field_name`` and ``lookup_expr`` represent a complete Django lookup expression. A detailed explanation of lookup expressions is provided in Django's `lookup reference`_. django-filter supports expressions containing both transforms and a final lookup. .. _`lookup reference`: https://docs.djangoproject.com/en/stable/ref/models/lookups/#module-django.db.models.lookups Generating filters with Meta.fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The FilterSet Meta class provides a ``fields`` attribute that can be used for easily specifying multiple filters without significant code duplication. The base syntax supports a list of multiple field names:: import django_filters class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = ['price', 'release_date'] The above generates 'exact' lookups for both the 'price' and 'release_date' fields. Additionally, a dictionary can be used to specify multiple lookup expressions for each field:: import django_filters class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = { 'price': ['lt', 'gt'], 'release_date': ['exact', 'year__gt'], } The above would generate 'price__lt', 'price__gt', 'release_date', and 'release_date__year__gt' filters. .. note:: The filter lookup type 'exact' is an implicit default and therefore never added to a filter name. In the above example, the release date's exact filter is 'release_date', not 'release_date__exact'. This can be overridden by the FILTERS_DEFAULT_LOOKUP_EXPR setting. Items in the ``fields`` sequence in the ``Meta`` class may include "relationship paths" using Django's ``__`` syntax to filter on fields on a related model:: class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = ['manufacturer__country'] Overriding default filters """""""""""""""""""""""""" Like ``django.contrib.admin.ModelAdmin``, it is possible to override default filters for all the models fields of the same kind using ``filter_overrides`` on the ``Meta`` class:: class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = { 'name': ['exact'], 'release_date': ['isnull'], } filter_overrides = { models.CharField: { 'filter_class': django_filters.CharFilter, 'extra': lambda f: { 'lookup_expr': 'icontains', }, }, models.BooleanField: { 'filter_class': django_filters.BooleanFilter, 'extra': lambda f: { 'widget': forms.CheckboxInput, }, }, } Request-based filtering ~~~~~~~~~~~~~~~~~~~~~~~ The ``FilterSet`` may be initialized with an optional ``request`` argument. If a request object is passed, then you may access the request during filtering. This allows you to filter by properties on the request, such as the currently logged-in user or the ``Accepts-Languages`` header. .. note:: It is not guaranteed that a `request` will be provided to the `FilterSet` instance. Any code depending on a request should handle the `None` case. Filtering the primary ``.qs`` """"""""""""""""""""""""""""" To filter the primary queryset by the ``request`` object, simply override the ``FilterSet.qs`` property. For example, you could filter blog articles to only those that are published and those that are owned by the logged-in user (presumably the author's draft articles). .. code-block:: python class ArticleFilter(django_filters.FilterSet): class Meta: model = Article fields = [...] @property def qs(self): parent = super().qs author = getattr(self.request, 'user', None) return parent.filter(is_published=True) \ | parent.filter(author=author) Filtering the related queryset for ``ModelChoiceFilter`` """""""""""""""""""""""""""""""""""""""""""""""""""""""" The ``queryset`` argument for ``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter`` supports callable behavior. If a callable is passed, it will be invoked with the ``request`` as its only argument. This allows you to perform the same kinds of request-based filtering without resorting to overriding ``FilterSet.__init__``. .. code-block:: python def departments(request): if request is None: return Department.objects.none() company = request.user.company return company.department_set.all() class EmployeeFilter(filters.FilterSet): department = filters.ModelChoiceFilter(queryset=departments) ... Customize filtering with ``Filter.method`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can control the behavior of a filter by specifying a ``method`` to perform filtering. View more information in the :ref:`method reference `. Note that you may access the filterset's properties, such as the ``request``. .. code-block:: python class F(django_filters.FilterSet): username = CharFilter(method='my_custom_filter') class Meta: model = User fields = ['username'] def my_custom_filter(self, queryset, name, value): return queryset.filter(**{ name: value, }) The view -------- Now we need to write a view:: def product_list(request): f = ProductFilter(request.GET, queryset=Product.objects.all()) return render(request, 'my_app/template.html', {'filter': f}) If a queryset argument isn't provided then all the items in the default manager of the model will be used. If you want to access the filtered objects in your views, for example if you want to paginate them, you can do that. They are in f.qs The URL conf ------------ We need a URL pattern to call the view:: path('list/', views.product_list, name="product-list") The template ------------ And lastly we need a template:: {% extends "base.html" %} {% block content %}
    {{ filter.form.as_p }}
    {% for obj in filter.qs %} {{ obj.name }} - ${{ obj.price }}
    {% endfor %} {% endblock %} And that's all there is to it! The ``form`` attribute contains a normal Django form, and when we iterate over the ``FilterSet.qs`` we get the objects in the resulting queryset. Generic view & configuration ----------------------------- In addition to the above usage there is also a class-based generic view included in django-filter, which lives at ``django_filters.views.FilterView``. You must provide either a ``model`` or ``filterset_class`` argument, similar to ``ListView`` in Django itself:: # urls.py from django.urls import path from django_filters.views import FilterView from myapp.models import Product urlpatterns = [ path("list/", FilterView.as_view(model=Product), name="product-list"), ] If you provide a ``model`` optionally you can set ``filterset_fields`` to specify a list or a tuple of the fields that you want to include for the automatic construction of the filterset class. You must provide a template at ``/_filter.html`` which gets the context parameter ``filter``. Additionally, the context will contain ``object_list`` which holds the filtered queryset. A legacy functional generic view is still included in django-filter, although its use is deprecated. It can be found at ``django_filters.views.object_filter``. You must provide the same arguments to it as the class based view:: # urls.py from django.urls import path from django_filters.views import object_filter from myapp.models import Product urlpatterns = [ path("list/", object_filter, {'model': Product}, name="product-list"), ] The needed template and its context variables will also be the same as the class-based view above. django-filter-23.5/docs/guide/tips.txt0000644000175000017500000002427514533556010017631 0ustar carstencarsten================== Tips and Solutions ================== Common problems for declared filters ------------------------------------ Below are some of the common problems that occur when declaring filters. It is recommended that you read this as it provides a more complete understanding of how filters work. Filter ``field_name`` and ``lookup_expr`` not configured ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ While ``field_name`` and ``lookup_expr`` are optional, it is recommended that you specify them. By default, if ``field_name`` is not specified, the filter's name on the ``FilterSet`` class will be used. Additionally, ``lookup_expr`` defaults to ``exact``. The following is an example of a misconfigured price filter: .. code-block:: python class ProductFilter(django_filters.FilterSet): price__gt = django_filters.NumberFilter() The filter instance will have a field name of ``price__gt`` and an ``exact`` lookup type. Under the hood, this will incorrectly be resolved as: .. code-block:: python Product.objects.filter(price__gt__exact=value) The above will most likely generate a ``FieldError``. The correct configuration would be: .. code-block:: python class ProductFilter(django_filters.FilterSet): price__gt = django_filters.NumberFilter(field_name='price', lookup_expr='gt') When using ``filterset_fields``, you can also add the ``lookup_expr`` in the dict of fields like so: .. code-block:: python # ... ModelViewSet with DjangoFilterBackend in filter_backends ... filterset_fields = { "price": ["gt", "exact"], } Missing ``lookup_expr`` for text search filters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's quite common to forget to set the lookup expression for :code:`CharField` and :code:`TextField` and wonder why a search for "foo" does not return results for "foobar". This is because the default lookup type is ``exact``, but you probably want to perform an ``icontains`` lookup. Filter and lookup expression mismatch (in, range, isnull) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's not always appropriate to directly match a filter to its model field's type, as some lookups expect different types of values. This is a commonly found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look at the following product model: .. code-block:: python class Product(models.Model): category = models.ForeignKey(Category, null=True) Given that ``category`` is optional, it's reasonable to want to enable a search for uncategorized products. The following is an incorrectly configured ``isnull`` filter: .. code-block:: python class ProductFilter(django_filters.FilterSet): uncategorized = django_filters.NumberFilter(field_name='category', lookup_expr='isnull') So what's the issue? While the underlying column type for ``category`` is an integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however only validates numbers. Filters are not `'expression aware'` and won't change behavior based on their ``lookup_expr``. You should use filters that match the data type of the lookup expression `instead` of the data type underlying the model field. The following would correctly allow you to search for both uncategorized products and products for a set of categories: .. code-block:: python class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): pass class ProductFilter(django_filters.FilterSet): categories = NumberInFilter(field_name='category', lookup_expr='in') uncategorized = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull') More info on constructing ``in`` and ``range`` csv :ref:`filters `. Filtering by empty values ------------------------- There are a number of cases where you may need to filter by empty or null values. The following are some common solutions to these problems: Filtering by null values ~~~~~~~~~~~~~~~~~~~~~~~~ As explained in the above "Filter and lookup expression mismatch" section, a common problem is how to correctly filter by null values on a field. Solution 1: Using a ``BooleanFilter`` with ``isnull`` """"""""""""""""""""""""""""""""""""""""""""""""""""" Using ``BooleanFilter`` with an ``isnull`` lookup is a builtin solution used by the FilterSet's automatic filter generation. To do this manually, simply add: .. code-block:: python class ProductFilter(django_filters.FilterSet): uncategorized = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull') .. note:: Remember that the filter class is validating the input value. The underlying type of the mode field is not relevant here. You may also reverse the logic with the ``exclude`` parameter. .. code-block:: python class ProductFilter(django_filters.FilterSet): has_category = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull', exclude=True) Solution 2: Using ``ChoiceFilter``'s null choice """""""""""""""""""""""""""""""""""""""""""""""" If you're using a ChoiceFilter, you may also filter by null values by enabling the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference :ref:`docs `. .. code-block:: python class ProductFilter(django_filters.FilterSet): category = django_filters.ModelChoiceFilter( field_name='category', lookup_expr='isnull', null_label='Uncategorized', queryset=Category.objects.all(), ) Solution 3: Combining fields w/ ``MultiValueField`` """"""""""""""""""""""""""""""""""""""""""""""""""" An alternative approach is to use Django's ``MultiValueField`` to manually add in a ``BooleanField`` to handle null values. Proof of concept: https://github.com/carltongibson/django-filter/issues/446 Filtering by an empty string ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's not currently possible to filter by an empty string, since empty values are interpreted as a skipped filter. GET http://localhost/api/my-model?myfield= Solution 1: Magic values """""""""""""""""""""""" You can override the ``filter()`` method of a filter class to specifically check for magic values. This is similar to the ``ChoiceFilter``'s null value handling. GET http://localhost/api/my-model?myfield=EMPTY .. code-block:: python class MyCharFilter(filters.CharFilter): empty_value = 'EMPTY' def filter(self, qs, value): if value != self.empty_value: return super().filter(qs, value) qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): ""}) return qs.distinct() if self.distinct else qs Solution 2: Empty string filter """"""""""""""""""""""""""""""" It would also be possible to create an empty value filter that exhibits the same behavior as an ``isnull`` filter. GET http://localhost/api/my-model?myfield__isempty=false .. code-block:: python from django.core.validators import EMPTY_VALUES class EmptyStringFilter(filters.BooleanFilter): def filter(self, qs, value): if value in EMPTY_VALUES: return qs exclude = self.exclude ^ (value is False) method = qs.exclude if exclude else qs.filter return method(**{self.field_name: ""}) class MyFilterSet(filters.FilterSet): myfield__isempty = EmptyStringFilter(field_name='myfield') class Meta: model = MyModel fields = [] Filtering by relative times --------------------------- Given a model with a timestamp field, it may be useful to filter based on relative times. For instance, perhaps we want to get data from the past *n* hours. This could be accomplished with a ``NumberFilter`` that invokes a custom method. .. code-block:: python from django.utils import timezone from datetime import timedelta ... class DataModel(models.Model): time_stamp = models.DateTimeField() class DataFilter(django_filters.FilterSet): hours = django_filters.NumberFilter( field_name='time_stamp', method='get_past_n_hours', label="Past n hours") def get_past_n_hours(self, queryset, field_name, value): time_threshold = timezone.now() - timedelta(hours=int(value)) return queryset.filter(time_stamp__gte=time_threshold) class Meta: model = DataModel fields = ('hours',) Using ``initial`` values as defaults ------------------------------------ In pre-1.0 versions of django-filter, a filter field's ``initial`` value was used as a default when no value was submitted. This behavior was not officially supported and has since been removed. .. warning:: It is recommended that you do **NOT** implement the below as it adversely affects usability. Django forms don't provide this behavior for a reason. - Using initial values as defaults is inconsistent with the behavior of Django forms. - Default values prevent users from filtering by empty values. - Default values prevent users from skipping that filter. If defaults are necessary though, the following should mimic the pre-1.0 behavior: .. code-block:: python class BaseFilterSet(FilterSet): def __init__(self, data=None, *args, **kwargs): # if filterset is bound, use initial values as defaults if data is not None: # get a mutable copy of the QueryDict data = data.copy() for name, f in self.base_filters.items(): initial = f.extra.get('initial') # filter param is either missing or empty, use initial as default if not data.get(name) and initial: data[name] = initial super().__init__(data, *args, **kwargs) Adding model field ``help_text`` to filters ------------------------------------------- Model field ``help_text`` is not used by filters by default. It can be added using a simple FilterSet base class:: class HelpfulFilterSet(django_filters.FilterSet): @classmethod def filter_for_field(cls, f, name, lookup_expr): filter = super(HelpfulFilterSet, cls).filter_for_field(f, name, lookup_expr) filter.extra['help_text'] = f.help_text return filter django-filter-23.5/docs/guide/rest_framework.txt0000644000175000017500000001727014533556010021701 0ustar carstencarsten.. _drf integration: ==================== Integration with DRF ==================== Integration with `Django Rest Framework`__ is provided through a DRF-specific ``FilterSet`` and a `filter backend`__. These may be found in the ``rest_framework`` sub-package. __ http://www.django-rest-framework.org/ __ http://www.django-rest-framework.org/api-guide/filtering/ Quickstart ---------- Using the new ``FilterSet`` simply requires changing the import path. Instead of importing from ``django_filters``, import from the ``rest_framework`` sub-package. .. code-block:: python from django_filters import rest_framework as filters class ProductFilter(filters.FilterSet): ... Your view class will also need to add ``DjangoFilterBackend`` to the ``filter_backends``. .. code-block:: python from django_filters import rest_framework as filters class ProductList(generics.ListAPIView): queryset = Product.objects.all() serializer_class = ProductSerializer filter_backends = (filters.DjangoFilterBackend,) filterset_fields = ('category', 'in_stock') If you want to use the django-filter backend by default, add it to the ``DEFAULT_FILTER_BACKENDS`` setting. .. code-block:: python # settings.py INSTALLED_APPS = [ ... 'rest_framework', 'django_filters', ] REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', ... ), } Adding a FilterSet with ``filterset_class`` ------------------------------------------- To enable filtering with a ``FilterSet``, add it to the ``filterset_class`` parameter on your view class. .. code-block:: python from rest_framework import generics from django_filters import rest_framework as filters from myapp import Product class ProductFilter(filters.FilterSet): min_price = filters.NumberFilter(field_name="price", lookup_expr='gte') max_price = filters.NumberFilter(field_name="price", lookup_expr='lte') class Meta: model = Product fields = ['category', 'in_stock'] class ProductList(generics.ListAPIView): queryset = Product.objects.all() serializer_class = ProductSerializer filter_backends = (filters.DjangoFilterBackend,) filterset_class = ProductFilter Using the ``filterset_fields`` shortcut --------------------------------------- You may bypass creating a ``FilterSet`` by instead adding ``filterset_fields`` to your view class. This is equivalent to creating a ``FilterSet`` with just :ref:`Meta.fields `. .. code-block:: python from rest_framework import generics from django_filters import rest_framework as filters from myapp import Product class ProductList(generics.ListAPIView): queryset = Product.objects.all() filter_backends = (filters.DjangoFilterBackend,) filterset_fields = ('category', 'in_stock') # Equivalent FilterSet: class ProductFilter(filters.FilterSet): class Meta: model = Product fields = ('category', 'in_stock') Note that using ``filterset_fields`` and ``filterset_class`` together is not supported. Overriding FilterSet creation ----------------------------- ``FilterSet`` creation can be customized by overriding the following methods on the backend class: * ``.get_filterset(self, request, queryset, view)`` * ``.get_filterset_class(self, view, queryset=None)`` * ``.get_filterset_kwargs(self, request, queryset, view)`` You can override these methods on a case-by-case basis for each view, creating unique backends, or these methods can be used to write your own hooks to the view class. .. code-block:: python class MyFilterBackend(filters.DjangoFilterBackend): def get_filterset_kwargs(self, request, queryset, view): kwargs = super().get_filterset_kwargs(request, queryset, view) # merge filterset kwargs provided by view class if hasattr(view, 'get_filterset_kwargs'): kwargs.update(view.get_filterset_kwargs()) return kwargs class BookFilter(filters.FilterSet): def __init__(self, *args, author=None, **kwargs): super().__init__(*args, **kwargs) # do something w/ author class BookViewSet(viewsets.ModelViewSet): filter_backends = [MyFilterBackend] filterset_class = BookFilter def get_filterset_kwargs(self): return { 'author': self.get_author(), } Schema Generation with Core API and Open API -------------------------------------------- The backend class integrates with DRF's schema generation by implementing ``get_schema_fields()`` and ``get_schema_operation_parameters()``. ``get_schema_fields()`` is automatically enabled when Core API is installed. ``get_schema_operation_parameters()`` is always enabled for Open API (new since DRF 3.9). Schema generation usually functions seamlessly, however the implementation does expect to invoke the view's ``get_queryset()`` method. There is a caveat in that views are artificially constructed during schema generation, so the ``args`` and ``kwargs`` attributes will be empty. If you depend on arguments parsed from the URL, you will need to handle their absence in ``get_queryset()``. For example, your get queryset method may look like this: .. code-block:: python class IssueViewSet(views.ModelViewSet): queryset = models.Issue.objects.all() def get_project(self): return models.Project.objects.get(pk=self.kwargs['project_id']) def get_queryset(self): project = self.get_project() return self.queryset \ .filter(project=project) \ .filter(author=self.request.user) This could be rewritten like so: .. code-block:: python class IssueViewSet(views.ModelViewSet): queryset = models.Issue.objects.all() def get_project(self): try: return models.Project.objects.get(pk=self.kwargs['project_id']) except models.Project.DoesNotExist: return None def get_queryset(self): project = self.get_project() if project is None: return self.queryset.none() return self.queryset \ .filter(project=project) \ .filter(author=self.request.user) Or more simply as: .. code-block:: python class IssueViewSet(views.ModelViewSet): queryset = models.Issue.objects.all() def get_queryset(self): # project_id may be None return self.queryset \ .filter(project_id=self.kwargs.get('project_id')) \ .filter(author=self.request.user) Crispy Forms ------------ If you are using DRF's browsable API or admin API you may also want to install ``django-crispy-forms``, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML. Note that this isn't actively supported, although pull requests for bug fixes are welcome. .. code-block:: bash pip install django-crispy-forms With crispy forms installed and added to Django's ``INSTALLED_APPS``, the browsable API will present a filtering control for ``DjangoFilterBackend``, like so: .. image:: ../assets/form.png Additional ``FilterSet`` Features --------------------------------- The following features are specific to the rest framework FilterSet: - ``BooleanFilter``'s use the API-friendly ``BooleanWidget``, which accepts lowercase ``true``/``false``. - Filter generation uses ``IsoDateTimeFilter`` for datetime model fields. - Raised ``ValidationError``'s are reraised as their DRF equivalent. django-filter-23.5/docs/conf.py0000644000175000017500000001765314533556010016315 0ustar carstencarsten# -*- coding: utf-8 -*- # # django-filter documentation build configuration file, created by # sphinx-quickstart on Mon Sep 17 11:25:20 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys from django_filters import __version__ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".txt" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = u"django-filter" copyright = u"2022, Alex Gaynor, Carlton Gibson and others." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "django-filterdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "django-filter.tex", u"django-filter Documentation", u"Alex Gaynor and others.", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "django-filter", u"django-filter Documentation", [u"Alex Gaynor and others."], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "django-filter", u"django-filter Documentation", u"Alex Gaynor and others.", "django-filter", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' django-filter-23.5/docs/make.bat0000644000175000017500000001176614533556010016422 0ustar carstencarsten@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-filter.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-filter.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end django-filter-23.5/docs/ref/0000755000175000017500000000000014533556010015556 5ustar carstencarstendjango-filter-23.5/docs/ref/fields.txt0000644000175000017500000000152214533556010017565 0ustar carstencarsten=============== Field Reference =============== ``IsoDateTimeField`` ~~~~~~~~~~~~~~~~~~~~ Extends ``django.forms.DateTimeField`` to allow parsing ISO 8601 formated dates, in addition to existing formats Defines a class level attribute ``ISO_8601`` as constant for the format. Sets ``input_formats = [ISO_8601]`` — this means that by default ``IsoDateTimeField`` will **only** parse ISO 8601 formated dates. You may set ``input_formats`` to your list of required formats as per the `DateTimeField Docs`_, using the ``ISO_8601`` class level attribute to specify the ISO 8601 format. .. code-block:: python f = IsoDateTimeField() f.input_formats = [IsoDateTimeField.ISO_8601] + DateTimeField.input_formats .. _`DateTimeField Docs`: https://docs.djangoproject.com/en/stable/ref/forms/fields/#django.forms.DateTimeField.input_formats django-filter-23.5/docs/ref/filterset.txt0000644000175000017500000001466714533556010020336 0ustar carstencarsten================= FilterSet Options ================= This document provides a guide on using additional FilterSet features. Meta options ------------ - :ref:`model ` - :ref:`fields ` - :ref:`exclude ` - :ref:`form
    ` - :ref:`filter_overrides ` .. _model: Automatic filter generation with ``model`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``FilterSet`` is capable of automatically generating filters for a given ``model``'s fields. Similar to Django's ``ModelForm``, filters are created based on the underlying model field's type. This option must be combined with either the ``fields`` or ``exclude`` option, which is the same requirement for Django's ``ModelForm`` class, detailed `here`__. __ https://docs.djangoproject.com/en/stable/topics/forms/modelforms/#selecting-the-fields-to-use .. code-block:: python class UserFilter(django_filters.FilterSet): class Meta: model = User fields = ['username', 'last_login'] .. _fields: Declaring filterable ``fields`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``fields`` option is combined with ``model`` to automatically generate filters. Note that generated filters will not overwrite filters declared on the ``FilterSet``. The ``fields`` option accepts two syntaxes: * a list of field names * a dictionary of field names mapped to a list of lookups .. code-block:: python class UserFilter(django_filters.FilterSet): class Meta: model = User fields = ['username', 'last_login'] # or class UserFilter(django_filters.FilterSet): class Meta: model = User fields = { 'username': ['exact', 'contains'], 'last_login': ['exact', 'year__gt'], } The list syntax will create an ``exact`` lookup filter for each field included in ``fields``. The dictionary syntax will create a filter for each lookup expression declared for its corresponding model field. These expressions may include both transforms and lookups, as detailed in the `lookup reference`__. __ https://docs.djangoproject.com/en/stable/ref/models/lookups/#module-django.db.models.lookups Note that it is **not** necessary to include declared filters in a ``fields`` list - doing so will only affect the order in which fields appear on a FilterSet's form. Including declarative aliases in a ``fields`` dict will raise an error. .. code-block:: python class UserFilter(django_filters.FilterSet): username = filters.CharFilter() login_timestamp = filters.IsoDateTimeFilter(field_name='last_login') class Meta: model = User fields = { 'username': ['exact', 'contains'], 'login_timestamp': ['exact'], } TypeError("'Meta.fields' contains fields that are not defined on this FilterSet: login_timestamp") .. _exclude: Disable filter fields with ``exclude`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``exclude`` option accepts a blacklist of field names to exclude from automatic filter generation. Note that this option will not disable filters declared directly on the ``FilterSet``. .. code-block:: python class UserFilter(django_filters.FilterSet): class Meta: model = User exclude = ['password'] .. _form: Custom Forms using ``form`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The inner ``Meta`` class also takes an optional ``form`` argument. This is a form class from which ``FilterSet.form`` will subclass. This works similar to the ``form`` option on a ``ModelAdmin.`` .. _filter_overrides: Customise filter generation with ``filter_overrides`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The inner ``Meta`` class also takes an optional ``filter_overrides`` argument. This is a map of model fields to filter classes with options:: class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = ['name', 'release_date'] filter_overrides = { models.CharField: { 'filter_class': django_filters.CharFilter, 'extra': lambda f: { 'lookup_expr': 'icontains', }, }, models.BooleanField: { 'filter_class': django_filters.BooleanFilter, 'extra': lambda f: { 'widget': forms.CheckboxInput, }, }, } Overriding ``FilterSet`` methods -------------------------------- When overriding classmethods, calling ``super(MyFilterSet, cls)`` may result in a ``NameError`` exception. This is due to the ``FilterSetMetaclass`` calling these classmethods before the ``FilterSet`` class has been fully created. There are two recommmended workarounds: 1. If using python 3.6 or newer, use the argumentless ``super()`` syntax. 2. For older versions of python, use an intermediate class. Ex:: class Intermediate(django_filters.FilterSet): @classmethod def method(cls, arg): super(Intermediate, cls).method(arg) ... class ProductFilter(Intermediate): class Meta: model = Product fields = ['...'] ``filter_for_lookup()`` ~~~~~~~~~~~~~~~~~~~~~~~ Prior to version 0.13.0, filter generation did not take into account the ``lookup_expr`` used. This commonly caused malformed filters to be generated for 'isnull', 'in', and 'range' lookups (as well as transformed lookups). The current implementation provides the following behavior: - 'isnull' lookups return a ``BooleanFilter`` - 'in' lookups return a filter derived from the CSV-based ``BaseInFilter``. - 'range' lookups return a filter derived from the CSV-based ``BaseRangeFilter``. If you want to override the ``filter_class`` and ``params`` used to instantiate filters for a model field, you can override ``filter_for_lookup()``. Ex:: class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = { 'release_date': ['exact', 'range'], } @classmethod def filter_for_lookup(cls, f, lookup_type): # override date range lookups if isinstance(f, models.DateField) and lookup_type == 'range': return django_filters.DateRangeFilter, {} # use default behavior otherwise return super().filter_for_lookup(f, lookup_type) django-filter-23.5/docs/ref/widgets.txt0000644000175000017500000000460314533556010017770 0ustar carstencarsten================ Widget Reference ================ This is a reference document with a list of the provided widgets and their arguments. .. _link-widget: ``LinkWidget`` ~~~~~~~~~~~~~~ This widget renders each option as a link, instead of an actual . It has one method that you can override for additional customizability. ``option_string()`` should return a string with 3 Python keyword argument placeholders: 1. ``attrs``: This is a string with all the attributes that will be on the final ```` tag. 2. ``query_string``: This is the query string for use in the ``href`` option on the ```` element. 3. ``label``: This is the text to be displayed to the user. .. _boolean-widget: ``BooleanWidget`` ~~~~~~~~~~~~~~~~~ This widget converts its input into Python's True/False values. It will convert all case variations of ``True`` and ``False`` into the internal Python values. To use it, pass this into the ``widgets`` argument of the ``BooleanFilter``: .. code-block:: python active = BooleanFilter(widget=BooleanWidget()) .. _csv-widget: ``CSVWidget`` ~~~~~~~~~~~~~ This widget expects a comma separated value and converts it into a list of string values. It is expected that the field class handle a list of values as well as type conversion. .. _range-widget: ``RangeWidget`` ~~~~~~~~~~~~~~~ This widget is used with ``RangeFilter`` and its subclasses. It generates two form input elements which generally act as start/end values in a range. Under the hood, it is Django's ``forms.TextInput`` widget and accepts the same arguments and values. To use it, pass it to ``widget`` argument of a ``RangeField``: .. code-block:: python date_range = DateFromToRangeFilter(widget=RangeWidget(attrs={'placeholder': 'YYYY/MM/DD'})) ``SuffixedMultiWidget`` ~~~~~~~~~~~~~~~~~~~~~~~ Extends Django's builtin ``MultiWidget`` to append custom suffixes instead of indices. For example, take a range widget that accepts minimum and maximum bounds. By default, the resulting query params would look like the following: .. code-block:: http GET /products?price_0=10&price_1=25 HTTP/1.1 By using ``SuffixedMultiWidget`` instead, you can provide human-friendly suffixes. .. code-block:: python class RangeWidget(SuffixedMultiWidget): suffixes = ['min', 'max'] The query names are now a little more ergonomic. .. code-block:: http GET /products?price_min=10&price_max=25 HTTP/1.1 django-filter-23.5/docs/ref/settings.txt0000644000175000017500000000522714533556010020165 0ustar carstencarsten================== Settings Reference ================== Here is a list of all available settings of django-filters and their default values. All settings are prefixed with ``FILTERS_``, although this is a bit verbose it helps to make it easy to identify these settings. FILTERS_DEFAULT_LOOKUP_EXPR --------------------------- Default: ``'exact'`` Set the default lookup expression to be generated, when none is defined. FILTERS_EMPTY_CHOICE_LABEL -------------------------- Default: ``'---------'`` Set the default value for ``ChoiceFilter.empty_label``. You may disable the empty choice by setting this to ``None``. FILTERS_NULL_CHOICE_LABEL ------------------------- Default: ``None`` Set the default value for ``ChoiceFilter.null_label``. You may enable the null choice by setting a non-``None`` value. FILTERS_NULL_CHOICE_VALUE ------------------------- Default: ``'null'`` Set the default value for ``ChoiceFilter.null_value``. You may want to change this value if the default ``'null'`` string conflicts with an actual choice. FILTERS_DISABLE_HELP_TEXT ------------------------- Default: ``False`` Some filters provide informational ``help_text``. For example, csv-based filters (``filters.BaseCSVFilter``) inform users that "Multiple values may be separated by commas". You may set this to ``True`` to disable the ``help_text`` for **all** filters, removing the text from the rendered form's output. .. _verbose-lookups-setting: FILTERS_VERBOSE_LOOKUPS ----------------------- .. note:: This is considered an advanced setting and is subject to change. Default: .. code-block:: python # refer to 'django_filters.conf.DEFAULTS' 'VERBOSE_LOOKUPS': { 'exact': _(''), 'iexact': _(''), 'contains': _('contains'), 'icontains': _('contains'), ... } This setting controls the verbose output for generated filter labels. Instead of getting expression parts such as "lt" and "contained_by", the verbose label would contain "is less than" and "is contained by". Verbose output may be disabled by setting this to a falsy value. This setting also accepts callables. The callable should not require arguments and should return a dictionary. This is useful for extending or overriding the default terms without having to copy the entire set of terms to your settings. For example, you could add verbose output for "exact" lookups. .. code-block:: python # settings.py def FILTERS_VERBOSE_LOOKUPS(): from django_filters.conf import DEFAULTS verbose_lookups = DEFAULTS['VERBOSE_LOOKUPS'].copy() verbose_lookups.update({ 'exact': 'is equal to', }) return verbose_lookups django-filter-23.5/docs/ref/filters.txt0000644000175000017500000006554714533556010020010 0ustar carstencarsten================ Filter Reference ================ This is a reference document with a list of the filters and their arguments. .. _core-arguments: Core Arguments -------------- The following are the core arguments that apply to all filters. Note that they are joined to construct the complete `lookup expression`_ that is the left hand side of the ORM ``.filter()`` call. .. _`lookup expression`: https://docs.djangoproject.com/en/stable/ref/models/lookups/#module-django.db.models.lookups ``field_name`` ~~~~~~~~~~~~~~ The name of the model field that is filtered against. If this argument is not provided, it defaults the filter's attribute name on the ``FilterSet`` class. Field names can traverse relationships by joining the related parts with the ORM lookup separator (``__``). e.g., a product's ``manufacturer__name``. ``lookup_expr`` ~~~~~~~~~~~~~~~ The `field lookup`_ that should be performed in the filter call. Defaults to ``exact``. The ``lookup_expr`` can contain transforms if the expression parts are joined by the ORM lookup separator (``__``). e.g., filter a datetime by its year part ``year__gt``. .. _`Field lookup`: https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups .. _keyword-only-arguments: Keyword-only Arguments ---------------------- The following are optional arguments that can be used to modify the behavior of all filters. ``label`` ~~~~~~~~~ The label as it will appear in the HTML, analogous to a form field's label argument. If a label is not provided, a verbose label will be generated based on the field ``field_name`` and the parts of the ``lookup_expr`` (see: :ref:`verbose-lookups-setting`). .. _filter-method: ``method`` ~~~~~~~~~~ An optional argument that tells the filter how to handle the queryset. It can accept either a callable or the name of a method on the ``FilterSet``. The callable receives a ``QuerySet``, the name of the model field to filter on, and the value to filter with. It should return a filtered ``Queryset``. Note that the value is validated by the ``Filter.field``, so raw value transformation and empty value checking should be unnecessary. .. code-block:: python class F(FilterSet): """Filter for Books by if books are published or not""" published = BooleanFilter(field_name='published_on', method='filter_published') def filter_published(self, queryset, name, value): # construct the full lookup expression. lookup = '__'.join([name, 'isnull']) return queryset.filter(**{lookup: False}) # alternatively, you could opt to hardcode the lookup. e.g., # return queryset.filter(published_on__isnull=False) class Meta: model = Book fields = ['published'] # Callables may also be defined out of the class scope. def filter_not_empty(queryset, name, value): lookup = '__'.join([name, 'isnull']) return queryset.filter(**{lookup: False}) class F(FilterSet): """Filter for Books by if books are published or not""" published = BooleanFilter(field_name='published_on', method=filter_not_empty) class Meta: model = Book fields = ['published'] ``distinct`` ~~~~~~~~~~~~ A boolean that specifies whether the Filter will use distinct on the queryset. This option can be used to eliminate duplicate results when using filters that span relationships. Defaults to ``False``. ``exclude`` ~~~~~~~~~~~ A boolean that specifies whether the Filter should use ``filter`` or ``exclude`` on the queryset. Defaults to ``False``. ``required`` ~~~~~~~~~~~~ A boolean that specifies whether the Filter is required or not. Defaults to ``False``. ``**kwargs`` ~~~~~~~~~~~~ Any additional keyword arguments are stored as the ``extra`` parameter on the filter. They are provided to the accompanying form ``Field`` and can be used to provide arguments like ``choices``. Some field-related arguments: ``widget`` """""""""" The django.form Widget class which will represent the ``Filter``. In addition to the widgets that are included with Django that you can use there are additional ones that django-filter provides which may be useful: * :ref:`LinkWidget ` -- this displays the options in a manner similar to the way the Django Admin does, as a series of links. The link for the selected option will have ``class="selected"``. * :ref:`BooleanWidget ` -- this widget converts its input into Python's True/False values. It will convert all case variations of ``True`` and ``False`` into the internal Python values. * :ref:`CSVWidget ` -- this widget expects a comma separated value and converts it into a list of string values. It is expected that the field class handle a list of values as well as type conversion. * :ref:`RangeWidget ` -- this widget is used with ``RangeFilter`` to generate two form input elements using a single field. ModelChoiceFilter and ModelMultipleChoiceFilter arguments --------------------------------------------------------- These arguments apply specifically to ModelChoiceFilter and ModelMultipleChoiceFilter only. ``queryset`` ~~~~~~~~~~~~ ``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter`` require a queryset to operate on which must be passed as a kwarg. ``to_field_name`` ~~~~~~~~~~~~~~~~~ If you pass in ``to_field_name`` (which gets forwarded to the Django field), it will be used also in the default ``get_filter_predicate`` implementation as the model's attribute. Filters ------- ``CharFilter`` ~~~~~~~~~~~~~~ This filter does simple character matches, used with ``CharField`` and ``TextField`` by default. ``UUIDFilter`` ~~~~~~~~~~~~~~ This filter matches UUID values, used with ``models.UUIDField`` by default. ``BooleanFilter`` ~~~~~~~~~~~~~~~~~ This filter matches a boolean, either ``True`` or ``False``, used with ``BooleanField`` and ``NullBooleanField`` by default. .. _choice-filter: ``ChoiceFilter`` ~~~~~~~~~~~~~~~~ This filter matches values in its ``choices`` argument. The ``choices`` must be explicitly passed when the filter is declared on the ``FilterSet``. For example, .. code-block:: python class User(models.Model): username = models.CharField(max_length=255) first_name = SubCharField(max_length=100) last_name = SubSubCharField(max_length=100) status = models.IntegerField(choices=STATUS_CHOICES, default=0) STATUS_CHOICES = ( (0, 'Regular'), (1, 'Manager'), (2, 'Admin'), ) class F(FilterSet): status = ChoiceFilter(choices=STATUS_CHOICES) class Meta: model = User fields = ['status'] ``ChoiceFilter`` also has arguments that enable a choice for not filtering, as well as a choice for filtering by ``None`` values. Each of the arguments have a corresponding global setting (:doc:`/ref/settings`). * ``empty_label``: The display label to use for the select choice to not filter. The choice may be disabled by setting this argument to ``None``. Defaults to ``FILTERS_EMPTY_CHOICE_LABEL``. * ``null_label``: The display label to use for the choice to filter by ``None`` values. The choice may be disabled by setting this argument to ``None``. Defaults to ``FILTERS_NULL_CHOICE_LABEL``. * ``null_value``: The special value to match to enable filtering by ``None`` values. This value defaults ``FILTERS_NULL_CHOICE_VALUE`` and needs to be a non-empty value (``''``, ``None``, ``[]``, ``()``, ``{}``). ``TypedChoiceFilter`` ~~~~~~~~~~~~~~~~~~~~~ The same as ``ChoiceFilter`` with the added possibility to convert value to match against. This could be done by using `coerce` parameter. An example use-case is limiting boolean choices to match against so only some predefined strings could be used as input of a boolean filter:: import django_filters from distutils.util import strtobool BOOLEAN_CHOICES = (('false', 'False'), ('true', 'True'),) class YourFilterSet(django_filters.FilterSet): ... flag = django_filters.TypedChoiceFilter(choices=BOOLEAN_CHOICES, coerce=strtobool) ``MultipleChoiceFilter`` ~~~~~~~~~~~~~~~~~~~~~~~~ The same as ``ChoiceFilter`` except the user can select multiple choices and the filter will form the OR of these choices by default to match items. The filter will form the AND of the selected choices when the ``conjoined=True`` argument is passed to this class. Multiple choices are represented in the query string by reusing the same key with different values (e.g. ''?status=Regular&status=Admin''). ``distinct`` defaults to ``True`` as to-many relationships will generally require this. Advanced Use: Depending on your application logic, when all or no choices are selected, filtering may be a noop. In this case you may wish to avoid the filtering overhead, particularly of the `distinct` call. Set `always_filter` to False after instantiation to enable the default `is_noop` test. Override `is_noop` if you require a different test for your application. ``TypedMultipleChoiceFilter`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Like ``MultipleChoiceFilter``, but in addition accepts the ``coerce`` parameter, as in ``TypedChoiceFilter``. ``DateFilter`` ~~~~~~~~~~~~~~ Matches on a date. Used with ``DateField`` by default. ``TimeFilter`` ~~~~~~~~~~~~~~ Matches on a time. Used with ``TimeField`` by default. ``DateTimeFilter`` ~~~~~~~~~~~~~~~~~~ Matches on a date and time. Used with ``DateTimeField`` by default. ``IsoDateTimeFilter`` ~~~~~~~~~~~~~~~~~~~~~ Uses ``IsoDateTimeField`` to support filtering on ISO 8601 formatted dates, as are often used in APIs, and are employed by default by Django REST Framework. Example:: class F(FilterSet): """Filter for Books by date published, using ISO 8601 formatted dates""" published = IsoDateTimeFilter() class Meta: model = Book fields = ['published'] ``DurationFilter`` ~~~~~~~~~~~~~~~~~~ Matches on a duration. Used with ``DurationField`` by default. Supports both Django ('%d %H:%M:%S.%f') and ISO 8601 formatted durations (but only the sections that are accepted by Python's timedelta, so no year, month, and week designators, e.g. 'P3DT10H22M'). ``ModelChoiceFilter`` ~~~~~~~~~~~~~~~~~~~~~ Similar to a ``ChoiceFilter`` except it works with related models, used for ``ForeignKey`` by default. If automatically instantiated, ``ModelChoiceFilter`` will use the default ``QuerySet`` for the related field. If manually instantiated you **must** provide the ``queryset`` kwarg. Example:: class F(FilterSet): """Filter for books by author""" author = ModelChoiceFilter(queryset=Author.objects.all()) class Meta: model = Book fields = ['author'] The ``queryset`` argument also supports callable behavior. If a callable is passed, it will be invoked with ``Filterset.request`` as its only argument. This allows you to easily filter by properties on the request object without having to override the ``FilterSet.__init__``. .. note:: You should expect that the `request` object may be `None`. .. code-block:: python def departments(request): if request is None: return Department.objects.none() company = request.user.company return company.department_set.all() class EmployeeFilter(filters.FilterSet): department = filters.ModelChoiceFilter(queryset=departments) ... ``ModelMultipleChoiceFilter`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Similar to a ``MultipleChoiceFilter`` except it works with related models, used for ``ManyToManyField`` by default. As with ``ModelChoiceFilter``, if automatically instantiated, ``ModelMultipleChoiceFilter`` will use the default ``QuerySet`` for the related field. If manually instantiated you **must** provide the ``queryset`` kwarg. Like ``ModelChoiceFilter``, the ``queryset`` argument has callable behavior. To use a custom field name for the lookup, you can use ``to_field_name``:: class FooFilter(BaseFilterSet): foo = django_filters.filters.ModelMultipleChoiceFilter( field_name='attr__uuid', to_field_name='uuid', queryset=Foo.objects.all(), ) If you want to use a custom queryset, e.g. to add annotated fields, this can be done as follows:: class MyMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter): def get_filter_predicate(self, v): return {'annotated_field': v.annotated_field} def filter(self, qs, value): if value: qs = qs.annotate_with_custom_field() qs = super().filter(qs, value) return qs foo = MyMultipleChoiceFilter( to_field_name='annotated_field', queryset=Model.objects.annotate_with_custom_field(), ) The ``annotate_with_custom_field`` method would be defined through a custom QuerySet, which then gets used as the model's manager:: class CustomQuerySet(models.QuerySet): def annotate_with_custom_field(self): return self.annotate( custom_field=Case( When(foo__isnull=False, then=F('foo__uuid')), When(bar__isnull=False, then=F('bar__uuid')), default=None, ), ) class MyModel(models.Model): objects = CustomQuerySet.as_manager() ``NumberFilter`` ~~~~~~~~~~~~~~~~ Filters based on a numerical value, used with ``IntegerField``, ``FloatField``, and ``DecimalField`` by default. .. method:: NumberFilter.get_max_validator() Return a ``MaxValueValidator`` instance that will be added to ``field.validators``. By default uses a limit value of ``1e50``. Return ``None`` to disable maximum value validation. ``NumericRangeFilter`` ~~~~~~~~~~~~~~~~~~~~~~ Filters where a value is between two numerical values, or greater than a minimum or less than a maximum where only one limit value is provided. This filter is designed to work with the Postgres Numerical Range Fields, including ``IntegerRangeField``, ``BigIntegerRangeField`` and ``FloatRangeField`` (available since Django 1.8). The default widget used is the ``RangeField``. Regular field lookups are available in addition to several containment lookups, including ``overlap``, ``contains``, and ``contained_by``. More details in the Django `docs`__. __ https://docs.djangoproject.com/en/stable/ref/contrib/postgres/fields/#querying-range-fields If the lower limit value is provided, the filter automatically defaults to ``startswith`` as the lookup and ``endswith`` if only the upper limit value is provided. ``RangeFilter`` ~~~~~~~~~~~~~~~ Filters where a value is between two numerical values, or greater than a minimum or less than a maximum where only one limit value is provided. :: class F(FilterSet): """Filter for Books by Price""" price = RangeFilter() class Meta: model = Book fields = ['price'] qs = Book.objects.all().order_by('title') # Range: Books between 5€ and 15€ f = F({'price_min': '5', 'price_max': '15'}, queryset=qs) # Min-Only: Books costing more the 11€ f = F({'price_min': '11'}, queryset=qs) # Max-Only: Books costing less than 19€ f = F({'price_max': '19'}, queryset=qs) ``DateRangeFilter`` ~~~~~~~~~~~~~~~~~~~ Filter similar to the admin changelist date one, it has a number of common selections for working with date fields. ``DateFromToRangeFilter`` ~~~~~~~~~~~~~~~~~~~~~~~~~ Similar to a ``RangeFilter`` except it uses dates instead of numerical values. It can be used with ``DateField``. It also works with ``DateTimeField``, but takes into consideration only the date. Example of using the ``DateField`` field:: class Comment(models.Model): date = models.DateField() time = models.TimeField() class F(FilterSet): date = DateFromToRangeFilter() class Meta: model = Comment fields = ['date'] # Range: Comments added between 2016-01-01 and 2016-02-01 f = F({'date_after': '2016-01-01', 'date_before': '2016-02-01'}) # Min-Only: Comments added after 2016-01-01 f = F({'date_after': '2016-01-01'}) # Max-Only: Comments added before 2016-02-01 f = F({'date_before': '2016-02-01'}) .. note:: When filtering ranges that occurs on DST transition dates ``DateFromToRangeFilter`` will use the first valid hour of the day for start datetime and the last valid hour of the day for end datetime. This is OK for most applications, but if you want to customize this behavior you must extend ``DateFromToRangeFilter`` and make a custom field for it. .. warning:: If you're using Django prior to 1.9 you may hit ``AmbiguousTimeError`` or ``NonExistentTimeError`` when start/end date matches DST start/end respectively. This occurs because versions before 1.9 don't allow to change the DST behavior for making a datetime aware. Example of using the ``DateTimeField`` field:: class Article(models.Model): published = models.DateTimeField() class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article fields = ['published'] Article.objects.create(published='2016-01-01 8:00') Article.objects.create(published='2016-01-20 10:00') Article.objects.create(published='2016-02-10 12:00') # Range: Articles published between 2016-01-01 and 2016-02-01 f = F({'published_after': '2016-01-01', 'published_before': '2016-02-01'}) assert len(f.qs) == 2 # Min-Only: Articles published after 2016-01-01 f = F({'published_after': '2016-01-01'}) assert len(f.qs) == 3 # Max-Only: Articles published before 2016-02-01 f = F({'published_before': '2016-02-01'}) assert len(f.qs) == 2 ``DateTimeFromToRangeFilter`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Similar to a ``RangeFilter`` except it uses datetime format values instead of numerical values. It can be used with ``DateTimeField``. Example:: class Article(models.Model): published = models.DateTimeField() class F(FilterSet): published = DateTimeFromToRangeFilter() class Meta: model = Article fields = ['published'] Article.objects.create(published='2016-01-01 8:00') Article.objects.create(published='2016-01-01 9:30') Article.objects.create(published='2016-01-02 8:00') # Range: Articles published 2016-01-01 between 8:00 and 10:00 f = F({'published_after': '2016-01-01 8:00', 'published_before': '2016-01-01 10:00'}) assert len(f.qs) == 2 # Min-Only: Articles published after 2016-01-01 8:00 f = F({'published_after': '2016-01-01 8:00'}) assert len(f.qs) == 3 # Max-Only: Articles published before 2016-01-01 10:00 f = F({'published_before': '2016-01-01 10:00'}) assert len(f.qs) == 2 ``IsoDateTimeFromToRangeFilter`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Similar to a ``RangeFilter`` except it uses ISO 8601 formatted values instead of numerical values. It can be used with ``IsoDateTimeField``. Example:: class Article(models.Model): published = django_filters.IsoDateTimeField() class F(FilterSet): published = IsoDateTimeFromToRangeFilter() class Meta: model = Article fields = ['published'] Article.objects.create(published='2016-01-01T8:00:00+01:00') Article.objects.create(published='2016-01-01T9:30:00+01:00') Article.objects.create(published='2016-01-02T8:00:00+01:00') # Range: Articles published 2016-01-01 between 8:00 and 10:00 f = F({'published_after': '2016-01-01T8:00:00+01:00', 'published_before': '2016-01-01T10:00:00+01:00'}) assert len(f.qs) == 2 # Min-Only: Articles published after 2016-01-01 8:00 f = F({'published_after': '2016-01-01T8:00:00+01:00'}) assert len(f.qs) == 3 # Max-Only: Articles published before 2016-01-01 10:00 f = F({'published_before': '2016-01-01T10:00:00+0100'}) assert len(f.qs) == 2 ``TimeRangeFilter`` ~~~~~~~~~~~~~~~~~~~ Similar to a ``RangeFilter`` except it uses time format values instead of numerical values. It can be used with ``TimeField``. Example:: class Comment(models.Model): date = models.DateField() time = models.TimeField() class F(FilterSet): time = TimeRangeFilter() class Meta: model = Comment fields = ['time'] # Range: Comments added between 8:00 and 10:00 f = F({'time_after': '8:00', 'time_before': '10:00'}) # Min-Only: Comments added after 8:00 f = F({'time_after': '8:00'}) # Max-Only: Comments added before 10:00 f = F({'time_before': '10:00'}) ``AllValuesFilter`` ~~~~~~~~~~~~~~~~~~~ This is a ``ChoiceFilter`` whose choices are the current values in the database. So if in the DB for the given field you have values of 5, 7, and 9 each of those is present as an option. This is similar to the default behavior of the admin. ``AllValuesMultipleFilter`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is a ``MultipleChoiceFilter`` whose choices are the current values in the database. So if in the DB for the given field you have values of 5, 7, and 9 each of those is present as an option. This is similar to the default behavior of the admin. .. _lookup-choice-filter: ``LookupChoiceFilter`` ~~~~~~~~~~~~~~~~~~~~~~ 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'), ] ) .. _base-in-filter: ``BaseInFilter`` ~~~~~~~~~~~~~~~~ This is a base class used for creating IN lookup filters. It is expected that this filter class is used in conjunction with another filter class, as this class **only** validates that the incoming value is comma-separated. The secondary filter is then used to validate the individual values. Example:: class NumberInFilter(BaseInFilter, NumberFilter): pass class F(FilterSet): id__in = NumberInFilter(field_name='id', lookup_expr='in') class Meta: model = User User.objects.create(username='alex') User.objects.create(username='jacob') User.objects.create(username='aaron') User.objects.create(username='carl') # In: User with IDs 1 and 3. f = F({'id__in': '1,3'}) assert len(f.qs) == 2 ``BaseRangeFilter`` ~~~~~~~~~~~~~~~~~~~ This is a base class used for creating RANGE lookup filters. It behaves identically to ``BaseInFilter`` with the exception that it expects only two comma-separated values. Example:: class NumberRangeFilter(BaseRangeFilter, NumberFilter): pass class F(FilterSet): id__range = NumberRangeFilter(field_name='id', lookup_expr='range') class Meta: model = User User.objects.create(username='alex') User.objects.create(username='jacob') User.objects.create(username='aaron') User.objects.create(username='carl') # Range: User with IDs between 1 and 3. f = F({'id__range': '1,3'}) assert len(f.qs) == 3 .. _ordering-filter: ``OrderingFilter`` ~~~~~~~~~~~~~~~~~~ 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. .. code-block:: python class UserFilter(FilterSet): account = CharFilter(field_name='username') status = NumberFilter(field_name='status') o = OrderingFilter( # tuple-mapping retains order fields=( ('username', 'account'), ('first_name', 'first_name'), ('last_name', 'last_name'), ), # labels do not need to retain order field_labels={ 'username': 'User account', } ) class Meta: model = User fields = ['first_name', 'last_name'] >>> UserFilter().filters['o'].field.choices [ ('account', 'User account'), ('-account', 'User account (descending)'), ('first_name', 'First name'), ('-first_name', 'First name (descending)'), ('last_name', 'Last name'), ('-last_name', 'Last name (descending)'), ] 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. .. code-block:: python class UserFilter(FilterSet): account = CharFilter(field_name='username') status = NumberFilter(field_name='status') o = OrderingFilter( choices=( ('account', 'Account'), ), fields={ 'username': 'account', }, ) 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. ``SelectMultiple`` widgets are not compatible, given that they are not able to retain selection order. Adding Custom filter choices """""""""""""""""""""""""""" If you wish to sort by non-model fields, you'll need to add custom handling to an ``OrderingFilter`` subclass. For example, if you want to sort by a computed 'relevance' factor, you would need to do something like the following: .. code-block:: python class CustomOrderingFilter(django_filters.OrderingFilter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.extra['choices'] += [ ('relevance', 'Relevance'), ('-relevance', 'Relevance (descending)'), ] def filter(self, qs, value): # OrderingFilter is CSV-based, so `value` is a list if any(v in ['relevance', '-relevance'] for v in value): # sort queryset by relevance return ... return super().filter(qs, value) django-filter-23.5/docs/Makefile0000644000175000017500000001303614533556010016445 0ustar carstencarsten# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-filter.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-filter.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-filter" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-filter" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." livehtml: sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html django-filter-23.5/docs/assets/0000755000175000017500000000000014533556010016304 5ustar carstencarstendjango-filter-23.5/docs/assets/form.png0000644000175000017500000003255614533556010017770 0ustar carstencarstenPNG  IHDRS iCCPICC ProfileHTLzBzG: lH(1B#+ ]QpQ!6 (bo;|s;s aRi`o7zdT4 y@ AAw}׿ƥrrR>DG(JCii.DX^iN;g#<%@AtNC#l.3v\36IIY5G6'yJ< >ˌTa2;\d= D>HD֬:iϝ?iq h9azgsJ@h`Tg + (pk& v0'@hep tp<` Q * i@1d 1 g򇂡((JZ m b :@@g z @[ ɰ< āPx9\x\ WF2 ߃W H(JebQhT냍&b` Vl/v;q8'\ K.np$ 91Em0~ C%8 \BpBE"LeD'b(1XJ#ߑH$-=i OH*%$]' >Fdw2|J~H~GP(hJerI*e&ŒJm*j-Z +͔^!-]"}J AFO]-^\L̄,UB6P6EP 9W.WA*MurG!yQ@|BBB~GcђiE}E5EbV:ۊ(*)++SLWTNRޡܤTbD%CeJ 8 4,x Q=ڥ:&T۫vEmLKUYKKIOү5U5}4Ś45'´r굞jڻ۴u4tt<%2ty{t;t?EmkWgg?164k5d&31lxFFac[c>^Ҥϔl4M7505^0zᎅ ۘ'1l!gkcbȒcYny׊beꍵu~6T-6m6llEuv:v1vv} yFqcfg[4?M9,_ȢA'-'!~gsA~MKsWmWk0Ӑ<|f&r;}{#ߣS3̳󙗖WW׸V>êaG +{o/o |v Ztn vIВ%/-wPCV Z8 L.,&cGDqDuQ*Qh\txtURϥ-YroPYJ앧b011bٕXVlE8ǝs+w/IpJؙ0s2D&M%G$קSbR IWe V^=.UBSKl A<^)# 595 230 niP(IDATx Uq5$rD.%nF ejJ^3%Ťh)j* 5jHPInK99>ϱαg^ǾiӦ_&L;v0ҥK{M H 0C=d+V^fXDLTΝUVtʖ-{ʧk.F} D_ TÆ ]ʩrѢEn?Tvu3]wevq={۷o_^ɓmyfShPs3' @@ S ÇwWeFiSr RZn@AaW^֥KSb;vJ= +V]Un:?~`={<6UYOMJڵ]Zt{OWeزeRڶm[j[0}g=æ|L{I6lIWN8f͚ u>]j.,z߃ך5k O<;ԃ  q"a*3㧟~&L`UVAGs_bC qkU)d [ouzmy7ey饗,%%?֨Q#ׯ;,&Jq-),*D⋮ $=q.PÇU}ݦnjrW{M6Bmzs)o֦MSշo_0` m@8re\RzI5j_vuYʕ\6 R/_>MRJc=fgu.̤[lsݮzԋN])j nJ .:{HFazL_ 9– ..4[ok6xlѢ ZjW=T?x:@@ mz=BZ<1c?w?')og, mwХ=ФR;xn|}')} >^^V-) / A c郔iNJfkV%  osRR%?MάhM/7TY.#w:zhȪ QSLqՋYQ(bgBDuԩ}i%MϨx}~i/g3f̰7|3ͼ @L d0hHTB&ku[ I =D:r^x;%72ۿI&n?`~(o(k'P> ĺ@ڸqk? 6YMU2sOVe0+s-\_sB =;ygܟթSK/4d .# +.Lݻjҵ#+UhQ‚{]SHEV:ά8\D&ڧ/EIV &ޛ7o[[NBWtMf8@3X ׅ)WHs䁟T_^de"w_4YsvrJt4MLtm@ȶ 歃 'KCMdd^VڊfDMP~E=wrnmWK_4_{nR]^Fk@bQ ׅ)~hMWz"tK /o>+޶G c?{PJyp]=<^;S+m۶[˷*(Dlev4[AS@'\4GkUqe]h=*tW[dVY |h̊&kTzFE̟?i䌪doiF4JW@ϯɛvy @, 0%\(EnQ]0\!+_}TZ3= [U Lj_N-4WF=C]|\Iۤ̚Yfnu2{) L}&Mrhy @, g_7ӧ[GJJj+Jaҽ}QYwХ{f]VoڴWae…GJ1zʷhWHLLt$$sx G ^QҍlBנA+\)pjbŋ={zfQ!tܹ"w-*o}G.4*kHV7F20+I@$\u=L6*/<޺߿owof͚K]*[l9daJâ0رcݽ {"{n ᜏn`c'5%EA@ =~ @iիv5\H]ݦ[D d` OkeJ<ݼt! ]CG{ɎaڵB5'/-ޙ}si@,pF ȱ! @| ߵn=  S@@S+  @7 D @]@@@@ Tx   "cW@@0o@@0" )~  @@ L@@" LEǮ  a  a*}?k~ d6muK,U=[gճ{ʗ/oƍ?NlܹVJ8pcڡCSN6.={Zڵ3fؔ)S /ۅG@aLR3guѮz[`=vQۺu 4ͳZjٯjC7|5!ĥKs=g'N@|rNJJ8pK1bo֬Y֯_?pwa77`]vQ]{]v^{J*evrgϞO>cQ}w.L\m_jMĞz)SGW\a_|<  QS :u… ]Xƍ,Y?#OsGiڵjҤIkRY{ܻw{l׮~S]Ϙ6;vmQhRPɓmR8RٿmذT/^f} TZj A@;L.\Pނ/(Q=GI 7ķl2{wuAE4TTp㕊+zO<.]ڽVhS&O0b)nF7oyRo,@5le˖uEujCKd4)nV볼6] Cj*  ķ@&@@ N\. ;  a*L8vC@$@w D @]@@@@ Tx   "cW@@ ̟?=@# fTSPMM@d:QB{ a   +@%  Z0چ-  aʗ   @hTh  )_"*  Sm؂  |  La   +@%  Z0چ-  aʗ   @hTh  )_"*  Sm؂  |  La   +@%  Z0چ-  aʗ   @hTh  )_"*  Sm؂  $Ȥ3&@@ J&g DGa* ĉa*NhN@#@+" @4@@ :* ĉa*NhN@#@+" @4@@ :* ĉa*NhN@#@+" @4@@ :* ĉa*NhN@#v:|5l0;#>|QF;u^oܸ1[>oĈ_=d{rnݺu؀ !y5Xb4UZ4E&M@V^j2M;ǏO:Y~ '-a{VjՓtsOZ D6vXKLLxʕ+gCUVYo߾VdI6/_>{1+_]f̘]IRݯ^ySW\ag}v&;O8쮻Jgyƚ6mjeʔqu>k׮K_z7nl>-Z駟oa6l… [G ڑ#G^S޽{yqֱcG۾} 2ĵSO94 @8>䓓BH.],99ٖ.]jk֬q!AAeŊO[su/4 f͚eҥKʕ+m̙.` 6̶nܶm/_[ B 0 L5˗𫯾jS[:u_VsB7엒ڝ3gժU:d c TTxTm㏻SKf͚6a0`s9.DN<}OuҐM yڱc}.5j+R+{@J(\`t]ZBW7tգzt>s G]ueU)  {WRyWsH{^ҖNHXyYOY|[ +PD]\OK&h- ?[*3/@@ re9>@85~F@  "cW@@0o@@0" )~  @@ L@@"v2Ϗc@4 i|Z70|G| 9.@'J诀a6lA@|SDT@@B B۰@ LQ@-@ m@@W0KD@@ a* [@@_”/@@6lA@|SDT@@B B۰@ LQ@-@ m@@W0KD@@ a* [@@_”/@@6lA@|SDT@@B B۰@ LQ@-@ m@@W0KD@@ a* [@@_$T?~&[ل ľ@@ @@0WZE@8 Li" @tSqU@T|ќ& DG0WZE@8 Li" @tSqU@T|ќ& DG0WZE@8v2v'狷ڴE[lՖ}qBvi_]Z*k8  @ };-;ZshB^KJJE;1ݑ'%%W{6rgۅ  #a󍜲yu@AR r  @]HN.@ԁC)q#g*P@@ ~SCę" Sm؂  |  La   +@%  Z0چ-  @Dm= gg% G2&@@ 0U`Z–|Οh'_vjUSz^[7+%{5Zdw$[s١q-̊u@@7:̗`"~nՅvc۾VY|i{SvZ`<`n1g;x/6yuoyU ]mٺ5Zܐu6.Kʓ`5=Vm=`vfU(b)G[@UQKR?'  @Fgjw ĨWk=-*!y9SӗjG!&1O;Lr yrv%l56tjZjnد7+Il=׽~tCx0<Ol7y#  RL(~nlXήQ,j^/7@UV ‘]?{kPoYm.ofw|͗ .ꩺ]GnXls@X mȸN1\n#k6: r)?K׃AJ;lڙTj+kf<\U:xD@NIആ kI=@]7y/緖aJn ~'0\4&ϛǏOF  a 0U`}ue+'\[Ṽyz%P .,N'М2:[w%W9  u$eÆn>Rլd`NM4WoUbLsLqW,Q)k> Oi6h{}9RnQgP@@?:@>X,x7JKh& \I]lF-h{KÅ>)ЫP޼2 9/0\#yD@2H% +;͊-aYyH$?ms-zdvt,xߜ|o^ֿeN QL ~{33 R^;<" 8sy" a*'L@T| a*'L@T| a*'L@TIU1D!  ď@aQ?RY8Syȅ #v޺%?bG*HAr  @};m۝l/jmU[ŏZ3=\kYUJ/n+/@@ " S ù! Y{/+S@uTÜ DU0U^G@X L7! @TSQq@uTÜ DU0U^G@X L7! @TSQq@uTÜ DU0U^G@X L7! @TSQq@uTÜ DU0U^G@X L7! @TcIENDB`django-filter-23.5/docs/index.txt0000644000175000017500000000123414533556010016652 0ustar carstencarsten============= django-filter ============= Django-filter is a generic, reusable application to alleviate writing some of the more mundane bits of view code. Specifically, it allows users to filter down a queryset based on a model's fields, displaying the form to let them do this. .. toctree:: :maxdepth: 2 :caption: User Guide guide/install guide/usage guide/rest_framework guide/tips guide/migration .. toctree:: :maxdepth: 1 :caption: Reference Documentation ref/filterset ref/filters ref/fields ref/widgets ref/settings .. toctree:: :maxdepth: 1 :caption: Developer Documentation dev/tests django-filter-23.5/requirements/0000755000175000017500000000000014533556010016575 5ustar carstencarstendjango-filter-23.5/requirements/docs.txt0000644000175000017500000000001614533556010020263 0ustar carstencarstenSphinx furo . django-filter-23.5/requirements/test-ci.txt0000644000175000017500000000010314533556010020700 0ustar carstencarstenmarkdown django-crispy-forms coverage pytz unittest-xml-reporting django-filter-23.5/requirements/test.txt0000644000175000017500000000005214533556010020312 0ustar carstencarsten-r test-ci.txt django djangorestframework django-filter-23.5/requirements/maintainer.txt0000644000175000017500000000002214533556010021457 0ustar carstencarstentwine wheel Sphinxdjango-filter-23.5/setup.cfg0000644000175000017500000000006314533556010015672 0ustar carstencarsten[flake8] max_line_length = 120 max_complexity = 10 django-filter-23.5/MANIFEST.in0000644000175000017500000000054414533556010015613 0ustar carstencarsteninclude AUTHORS include CHANGES.rst include LICENSE include README.rst include runshell.py include runtests.py recursive-include docs * recursive-include requirements * recursive-include tests * recursive-include django_filters/locale * recursive-include django_filters/templates *.html prune docs/_build global-exclude __pycache__ global-exclude *.py[co] django-filter-23.5/README.rst0000644000175000017500000000701314533556010015542 0ustar carstencarstenDjango 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://codecov.io/gh/carltongibson/django-filter/branch/develop/graph/badge.svg :target: https://codecov.io/gh/carltongibson/django-filter .. image:: https://badge.fury.io/py/django-filter.svg :target: http://badge.fury.io/py/django-filter Versioning and stability policy ------------------------------- Django-Filter is a mature and stable package. It uses a two-part CalVer versioning scheme, such as ``21.1``. The first number is the year. The second is the release number within that year. On an on-going basis, Django-Filter aims to support all current Django versions, the matching current Python versions, and the latest version of Django REST Framework. Please see: * `Status of supported Python versions `_ * `List of supported Django versions `_ Support for Python and Django versions will be dropped when they reach end-of-life. Support for Python versions will be dropped when they reach end-of-life, even when still supported by a current version of Django. Other breaking changes are rare. Where required, every effort will be made to apply a "Year plus two" deprecation period. For example, a change initially introduced in ``23.x`` would offer a fallback where feasible and finally be removed in ``25.1``. Where fallbacks are not feasible, breaking changes without deprecation will be called out in the release notes. Installation ------------ Install using pip: .. code-block:: sh pip install django-filter Then add ``'django_filters'`` to your ``INSTALLED_APPS``. .. code-block:: python INSTALLED_APPS = [ ... 'django_filters', ] Usage ----- Django-filter can be used for generating interfaces similar to the Django admin's ``list_filter`` interface. It has an API very similar to Django's ``ModelForms``. For example, if you had a Product model you could have a filterset for it with the code: .. code-block:: python import django_filters class ProductFilter(django_filters.FilterSet): class Meta: model = Product fields = ['name', 'price', 'manufacturer'] And then in your view you could do: .. code-block:: python def product_list(request): filter = ProductFilter(request.GET, queryset=Product.objects.all()) return render(request, 'my_app/template.html', {'filter': filter}) Usage with Django REST Framework -------------------------------- Django-filter provides a custom ``FilterSet`` and filter backend for use with Django REST Framework. To use this adjust your import to use ``django_filters.rest_framework.FilterSet``. .. code-block:: python from django_filters import rest_framework as filters class ProductFilter(filters.FilterSet): class Meta: model = Product fields = ('category', 'in_stock') For more details see the `DRF integration docs`_. Support ------- If you need help you can start a `discussion`_. For commercial support, please `contact Carlton Gibson via his website `_. .. _`discussion`: https://github.com/carltongibson/django-filter/discussions .. _`read the docs`: https://django-filter.readthedocs.io/en/main/ .. _`DRF integration docs`: https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html django-filter-23.5/Makefile0000644000175000017500000000022114533556010015505 0ustar carstencarsten.PHONY: deps, test, clean deps: pip install -r ./requirements/test.txt test: ./runtests.py clean: rm -r build dist django_filter.egg-info django-filter-23.5/.gitignore0000644000175000017500000000017514533556010016045 0ustar carstencarsten*.pyc *.egg-info build/ dist/ docs/_build .python-version .tox .coverage .coverage.* .xmlcoverage/ .venv/ .idea .env .vscode django-filter-23.5/AUTHORS0000644000175000017500000000051114533556010015117 0ustar carstencarstenAuthors ======= Thanks to the following people for contributing to django-filter. Ben Firshman Alex Gaynor Jannis Leidel Martin Mahner Brian Rosner Adam Vandenberg Florian Apolloner Andrew Ball Tino de Bruijn Maximillian Dornseif Marc Fargas Vladimir Sidorenko Tom Christie Remco Wendt Axel Haustant Brad Erickson Diogo Laginhadjango-filter-23.5/tests/0000755000175000017500000000000014533556010015214 5ustar carstencarstendjango-filter-23.5/tests/test_views.py0000644000175000017500000001255414533556010017771 0ustar carstencarstenfrom django.core.exceptions import ImproperlyConfigured from django.test import TestCase, override_settings from django.test.client import RequestFactory from django.utils import html from django_filters.filterset import FilterSet, filterset_factory from django_filters.views import FilterView from .models import Book @override_settings(ROOT_URLCONF="tests.urls") class GenericViewTestCase(TestCase): def setUp(self): Book.objects.create(title="Ender's Game", price="1.00", average_rating=3.0) Book.objects.create(title="Rainbow Six", price="1.00", average_rating=3.0) Book.objects.create(title="Snowcrash", price="1.00", average_rating=3.0) class GenericClassBasedViewTests(GenericViewTestCase): base_url = "/books/" def test_view(self): response = self.client.get(self.base_url) for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) def test_view_filtering_on_title(self): response = self.client.get(self.base_url + "?title=Snowcrash") for b in ["Ender's Game", "Rainbow Six"]: self.assertNotContains(response, html.escape(b)) self.assertContains(response, "Snowcrash") def test_view_with_filterset_not_model(self): factory = RequestFactory() request = factory.get(self.base_url) filterset = filterset_factory(Book) view = FilterView.as_view(filterset_class=filterset) response = view(request) self.assertEqual(response.status_code, 200) for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) def test_view_with_model_no_filterset(self): factory = RequestFactory() request = factory.get(self.base_url) view = FilterView.as_view(model=Book) response = view(request) self.assertEqual(response.status_code, 200) for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) def test_view_with_model_and_fields_no_filterset(self): factory = RequestFactory() request = factory.get(self.base_url + "?price=1.0") view = FilterView.as_view(model=Book, filterset_fields=["price"]) # filtering only by price response = view(request) self.assertEqual(response.status_code, 200) for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) # not filtering by title request = factory.get(self.base_url + "?title=Snowcrash") response = view(request) self.assertEqual(response.status_code, 200) for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) def test_view_with_strict_errors(self): factory = RequestFactory() request = factory.get(self.base_url + "?title=Snowcrash&price=four dollars") view = FilterView.as_view(model=Book) response = view(request) titles = [o.title for o in response.context_data["object_list"]] self.assertEqual(response.status_code, 200) self.assertEqual(titles, []) def test_view_with_non_strict_errors(self): factory = RequestFactory() request = factory.get(self.base_url + "?title=Snowcrash&price=four dollars") view = FilterView.as_view(model=Book, strict=False) response = view(request) titles = [o.title for o in response.context_data["object_list"]] self.assertEqual(response.status_code, 200) self.assertEqual( titles, ["Snowcrash"], ) def test_view_without_filterset_or_model(self): factory = RequestFactory() request = factory.get(self.base_url) view = FilterView.as_view() with self.assertRaises(ImproperlyConfigured): view(request) def test_view_with_bad_filterset(self): class MyFilterSet(FilterSet): pass factory = RequestFactory() request = factory.get(self.base_url) view = FilterView.as_view(filterset_class=MyFilterSet) with self.assertRaises(ImproperlyConfigured): view(request) def test_view_with_unbound_filter_form_returns_initial_queryset(self): factory = RequestFactory() request = factory.get(self.base_url) queryset = Book.objects.filter(title="Snowcrash") view = FilterView.as_view(model=Book, queryset=queryset) response = view(request) titles = [o.title for o in response.context_data["object_list"]] self.assertEqual(response.status_code, 200) self.assertEqual(titles, ["Snowcrash"]) class GenericFunctionalViewTests(GenericViewTestCase): base_url = "/books-legacy/" def test_view(self): response = self.client.get(self.base_url) for b in ["Ender's Game", "Rainbow Six", "Snowcrash"]: self.assertContains(response, html.escape(b)) # extra context self.assertEqual(response.context_data["foo"], "bar") self.assertEqual(response.context_data["bar"], "foo") def test_view_filtering_on_price(self): response = self.client.get(self.base_url + "?title=Snowcrash") for b in ["Ender's Game", "Rainbow Six"]: self.assertNotContains(response, html.escape(b)) self.assertContains(response, "Snowcrash") django-filter-23.5/tests/test_filterset.py0000644000175000017500000007676014533556010020646 0ustar carstencarstenimport unittest from unittest import mock from django.db import models from django.test import TestCase, override_settings from django_filters.exceptions import FieldLookupError from django_filters.filters import ( BaseInFilter, BaseRangeFilter, BooleanFilter, CharFilter, ChoiceFilter, DateRangeFilter, Filter, FilterMethod, ModelChoiceFilter, ModelMultipleChoiceFilter, NumberFilter, UUIDFilter, ) from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS, FilterSet from django_filters.widgets import BooleanWidget from .models import ( Account, AdminUser, Article, BankAccount, Book, Business, Comment, DirectedNode, NetworkSetting, Node, Profile, Restaurant, SubnetMaskField, User, UUIDTestModel, Worker, ) from .utils import MockQuerySet class HelperMethodsTests(TestCase): @unittest.skip("todo") def test_get_declared_filters(self): pass @unittest.skip("todo") def test_filters_for_model(self): pass @unittest.skip("todo") def test_filterset_factory(self): pass class DbFieldDefaultFiltersTests(TestCase): def test_expected_db_fields_get_filters(self): to_check = [ models.BooleanField, models.CharField, models.CommaSeparatedIntegerField, models.DateField, models.DateTimeField, models.DecimalField, models.EmailField, models.FilePathField, models.FloatField, models.IntegerField, models.GenericIPAddressField, models.NullBooleanField, models.PositiveIntegerField, models.PositiveSmallIntegerField, models.SlugField, models.SmallIntegerField, models.TextField, models.TimeField, models.DurationField, models.URLField, models.ForeignKey, models.OneToOneField, models.ManyToManyField, models.UUIDField, ] msg = "%s expected to be found in FILTER_FOR_DBFIELD_DEFAULTS" for m in to_check: self.assertIn(m, FILTER_FOR_DBFIELD_DEFAULTS, msg % m.__name__) def test_expected_db_fields_do_not_get_filters(self): to_check = [ models.Field, models.BigIntegerField, models.FileField, models.ImageField, ] msg = "%s expected to not be found in FILTER_FOR_DBFIELD_DEFAULTS" for m in to_check: self.assertNotIn(m, FILTER_FOR_DBFIELD_DEFAULTS, msg % m.__name__) class FilterSetFilterForFieldTests(TestCase): def test_filter_found_for_field(self): f = User._meta.get_field("username") result = FilterSet.filter_for_field(f, "username") self.assertIsInstance(result, CharFilter) self.assertEqual(result.field_name, "username") def test_filter_found_for_uuidfield(self): f = UUIDTestModel._meta.get_field("uuid") result = FilterSet.filter_for_field(f, "uuid") self.assertIsInstance(result, UUIDFilter) self.assertEqual(result.field_name, "uuid") def test_filter_found_for_autofield(self): f = User._meta.get_field("id") result = FilterSet.filter_for_field(f, "id") self.assertIsInstance(result, NumberFilter) self.assertEqual(result.field_name, "id") def test_field_with_extras(self): f = User._meta.get_field("favorite_books") result = FilterSet.filter_for_field(f, "favorite_books") self.assertIsInstance(result, ModelMultipleChoiceFilter) self.assertEqual(result.field_name, "favorite_books") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, Book) def test_field_with_choices(self): f = User._meta.get_field("status") result = FilterSet.filter_for_field(f, "status") self.assertIsInstance(result, ChoiceFilter) self.assertEqual(result.field_name, "status") self.assertTrue("choices" in result.extra) self.assertIsNotNone(result.extra["choices"]) def test_field_that_is_subclassed(self): f = User._meta.get_field("first_name") result = FilterSet.filter_for_field(f, "first_name") self.assertIsInstance(result, CharFilter) def test_unknown_field_type_error(self): f = NetworkSetting._meta.get_field("mask") with self.assertRaises(AssertionError) as excinfo: FilterSet.filter_for_field(f, "mask") self.assertIn( "FilterSet resolved field 'mask' with 'exact' lookup " "to an unrecognized field type SubnetMaskField", excinfo.exception.args[0], ) def test_symmetrical_selfref_m2m_field(self): f = Node._meta.get_field("adjacents") result = FilterSet.filter_for_field(f, "adjacents") self.assertIsInstance(result, ModelMultipleChoiceFilter) self.assertEqual(result.field_name, "adjacents") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, Node) def test_non_symmetrical_selfref_m2m_field(self): f = DirectedNode._meta.get_field("outbound_nodes") result = FilterSet.filter_for_field(f, "outbound_nodes") self.assertIsInstance(result, ModelMultipleChoiceFilter) self.assertEqual(result.field_name, "outbound_nodes") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, DirectedNode) def test_m2m_field_with_through_model(self): f = Business._meta.get_field("employees") result = FilterSet.filter_for_field(f, "employees") self.assertIsInstance(result, ModelMultipleChoiceFilter) self.assertEqual(result.field_name, "employees") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, Worker) def test_transformed_lookup_expr(self): f = Comment._meta.get_field("date") result = FilterSet.filter_for_field(f, "date", "year__gte") self.assertIsInstance(result, NumberFilter) self.assertEqual(result.field_name, "date") @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="icontains") def test_modified_default_lookup(self): f = User._meta.get_field("username") result = FilterSet.filter_for_field(f, "username") self.assertIsInstance(result, CharFilter) self.assertEqual(result.lookup_expr, "icontains") @unittest.skip("todo") def test_filter_overrides(self): pass class FilterSetFilterForLookupTests(TestCase): def test_filter_for_ISNULL_lookup(self): f = Article._meta.get_field("author") result, params = FilterSet.filter_for_lookup(f, "isnull") self.assertEqual(result, BooleanFilter) self.assertDictEqual(params, {}) def test_filter_for_IN_lookup(self): f = Article._meta.get_field("author") result, params = FilterSet.filter_for_lookup(f, "in") self.assertTrue(issubclass(result, ModelChoiceFilter)) self.assertTrue(issubclass(result, BaseInFilter)) self.assertEqual(params["to_field_name"], "id") def test_filter_for_RANGE_lookup(self): f = Article._meta.get_field("author") result, params = FilterSet.filter_for_lookup(f, "range") self.assertTrue(issubclass(result, ModelChoiceFilter)) self.assertTrue(issubclass(result, BaseRangeFilter)) self.assertEqual(params["to_field_name"], "id") def test_isnull_with_filter_overrides(self): class OFilterSet(FilterSet): class Meta: filter_overrides = { models.BooleanField: { "filter_class": BooleanFilter, "extra": lambda f: { "widget": BooleanWidget, }, }, } f = Article._meta.get_field("author") result, params = OFilterSet.filter_for_lookup(f, "isnull") self.assertEqual(result, BooleanFilter) self.assertEqual(params["widget"], BooleanWidget) class ReverseFilterSetFilterForFieldTests(TestCase): # Test reverse relationships for `filter_for_field` def test_reverse_o2o_relationship(self): f = Account._meta.get_field("profile") result = FilterSet.filter_for_field(f, "profile") self.assertIsInstance(result, ModelChoiceFilter) self.assertEqual(result.field_name, "profile") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, Profile) def test_reverse_fk_relationship(self): f = User._meta.get_field("comments") result = FilterSet.filter_for_field(f, "comments") self.assertIsInstance(result, ModelMultipleChoiceFilter) self.assertEqual(result.field_name, "comments") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, Comment) def test_reverse_m2m_relationship(self): f = Book._meta.get_field("lovers") result = FilterSet.filter_for_field(f, "lovers") self.assertIsInstance(result, ModelMultipleChoiceFilter) self.assertEqual(result.field_name, "lovers") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, User) def test_reverse_non_symmetrical_selfref_m2m_field(self): f = DirectedNode._meta.get_field("inbound_nodes") result = FilterSet.filter_for_field(f, "inbound_nodes") self.assertIsInstance(result, ModelMultipleChoiceFilter) self.assertEqual(result.field_name, "inbound_nodes") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, DirectedNode) def test_reverse_m2m_field_with_through_model(self): f = Worker._meta.get_field("employers") result = FilterSet.filter_for_field(f, "employers") self.assertIsInstance(result, ModelMultipleChoiceFilter) self.assertEqual(result.field_name, "employers") self.assertTrue("queryset" in result.extra) self.assertIsNotNone(result.extra["queryset"]) self.assertEqual(result.extra["queryset"].model, Business) def test_reverse_relationship_lookup_expr(self): f = Book._meta.get_field("lovers") result = FilterSet.filter_for_field(f, "lovers", "isnull") self.assertIsInstance(result, BooleanFilter) self.assertEqual(result.field_name, "lovers") self.assertEqual(result.lookup_expr, "isnull") class FilterSetClassCreationTests(TestCase): def test_no_filters(self): class F(FilterSet): pass self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 0) def test_declaring_filter(self): class F(FilterSet): username = CharFilter() self.assertEqual(len(F.declared_filters), 1) self.assertListEqual(list(F.declared_filters), ["username"]) self.assertEqual(len(F.base_filters), 1) self.assertListEqual(list(F.base_filters), ["username"]) @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="icontains") def test_declaring_filter_other_default_lookup(self): class F(FilterSet): username = CharFilter() self.assertEqual(F.base_filters["username"].lookup_expr, "icontains") def test_model_derived(self): class F(FilterSet): class Meta: model = Book fields = "__all__" self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 3) self.assertListEqual(list(F.base_filters), ["title", "price", "average_rating"]) @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="icontains") def test_model_derived_other_default_lookup(self): class F(FilterSet): class Meta: model = Book fields = "__all__" for filter_ in F.base_filters.values(): self.assertEqual(filter_.lookup_expr, "icontains") def test_model_no_fields_or_exclude(self): with self.assertRaises(AssertionError) as excinfo: class F(FilterSet): class Meta: model = Book self.assertIn( "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude'", str(excinfo.exception), ) def test_model_fields_empty(self): class F(FilterSet): class Meta: model = Book fields = [] self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 0) self.assertListEqual(list(F.base_filters), []) def test_model_exclude_empty(self): # equivalent to fields = '__all__' class F(FilterSet): class Meta: model = Book exclude = [] self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 3) self.assertListEqual(list(F.base_filters), ["title", "price", "average_rating"]) def test_declared_and_model_derived(self): class F(FilterSet): username = CharFilter() class Meta: model = Book fields = "__all__" self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 4) self.assertListEqual( list(F.base_filters), ["title", "price", "average_rating", "username"] ) def test_meta_fields_with_declared_and_model_derived(self): class F(FilterSet): username = CharFilter() class Meta: model = Book fields = ("username", "price") self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 2) self.assertListEqual(list(F.base_filters), ["username", "price"]) def test_meta_fields_dictionary_derived(self): class F(FilterSet): class Meta: model = Book fields = { "price": ["exact", "gte", "lte"], } self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 3) expected_list = [ "price", "price__gte", "price__lte", ] self.assertCountEqual(list(F.base_filters), expected_list) @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="lte") def test_meta_fields_dictionary_derived_other_default_lookup(self): class F(FilterSet): class Meta: model = Book fields = { "price": ["exact", "gte", "lte"], } self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 3) expected_list = [ "price__exact", "price__gte", "price", ] self.assertCountEqual(list(F.base_filters), expected_list) def test_meta_fields_containing_autofield(self): class F(FilterSet): username = CharFilter() class Meta: model = Book fields = ("id", "username", "price") self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 3) self.assertListEqual(list(F.base_filters), ["id", "username", "price"]) def test_meta_fields_dictionary_autofield(self): class F(FilterSet): username = CharFilter() class Meta: model = User fields = { "id": ["exact"], "username": ["exact"], } self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 2) expected_list = ["id", "username"] self.assertCountEqual(list(F.base_filters), expected_list) def test_meta_fields_list_containing_unknown_fields(self): msg = "'Meta.fields' must not contain non-model field names: " "other, another" with self.assertRaisesMessage(TypeError, msg): class F(FilterSet): username = CharFilter() class Meta: model = Book fields = ("username", "price", "other", "another") def test_meta_fields_dict_containing_unknown_fields(self): msg = "'Meta.fields' must not contain non-model field names: other" with self.assertRaisesMessage(TypeError, msg): class F(FilterSet): class Meta: model = Book fields = { "id": ["exact"], "title": ["exact"], "other": ["exact"], } def test_meta_fields_dict_containing_declarative_alias(self): # Meta.fields dict cannot generate lookups for an *aliased* field msg = "'Meta.fields' must not contain non-model field names: other" with self.assertRaisesMessage(TypeError, msg): class F(FilterSet): other = CharFilter() class Meta: model = Book fields = { "id": ["exact"], "title": ["exact"], "other": ["exact"], } def test_meta_fields_invalid_lookup(self): # We want to ensure that non existent lookups (or just simple misspellings) # throw a useful exception containing the field and lookup expr. msg = "Unsupported lookup 'flub' for field 'tests.User.username'." with self.assertRaisesMessage(FieldLookupError, msg): class F(FilterSet): class Meta: model = User fields = {"username": ["flub"]} def test_meta_exlude_with_declared_and_declared_wins(self): class F(FilterSet): username = CharFilter() class Meta: model = Book exclude = ("username", "price") self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 3) self.assertListEqual( list(F.base_filters), ["title", "average_rating", "username"] ) def test_meta_fields_and_exlude_and_exclude_wins(self): class F(FilterSet): username = CharFilter() class Meta: model = Book fields = ("username", "title", "price") exclude = ("title",) self.assertEqual(len(F.declared_filters), 1) self.assertEqual(len(F.base_filters), 2) self.assertListEqual(list(F.base_filters), ["username", "price"]) def test_meta_exlude_with_no_fields(self): class F(FilterSet): class Meta: model = Book exclude = ("price",) self.assertEqual(len(F.declared_filters), 0) self.assertEqual(len(F.base_filters), 2) self.assertListEqual(list(F.base_filters), ["title", "average_rating"]) def test_filterset_class_inheritance(self): class F(FilterSet): class Meta: model = Book fields = "__all__" class G(F): pass self.assertEqual(set(F.base_filters), set(G.base_filters)) class F(FilterSet): other = CharFilter class Meta: model = Book fields = "__all__" class G(F): pass self.assertEqual(set(F.base_filters), set(G.base_filters)) def test_abstract_model_inheritance(self): class F(FilterSet): class Meta: model = Restaurant fields = "__all__" self.assertEqual(set(F.base_filters), set(["name", "serves_pizza"])) class F(FilterSet): class Meta: model = Restaurant fields = ["name", "serves_pizza"] self.assertEqual(set(F.base_filters), set(["name", "serves_pizza"])) def test_custom_field_gets_filter_from_override(self): class F(FilterSet): class Meta: model = NetworkSetting fields = "__all__" filter_overrides = {SubnetMaskField: {"filter_class": CharFilter}} self.assertEqual(list(F.base_filters.keys()), ["ip", "mask", "cidr"]) def test_custom_declared_field_no_warning(self): class F(FilterSet): mask = CharFilter() class Meta: model = NetworkSetting fields = ["mask"] self.assertEqual(list(F.base_filters.keys()), ["mask"]) def test_filterset_for_proxy_model(self): class F(FilterSet): class Meta: model = User fields = "__all__" class ProxyF(FilterSet): class Meta: model = AdminUser fields = "__all__" self.assertEqual(list(F.base_filters), list(ProxyF.base_filters)) def test_filterset_for_mti_model(self): class F(FilterSet): class Meta: model = Account fields = "__all__" class FtiF(FilterSet): class Meta: model = BankAccount fields = "__all__" # fails due to 'account_ptr' getting picked up self.assertEqual( list(F.base_filters) + ["amount_saved"], list(FtiF.base_filters) ) def test_declared_filter_disabling(self): class Parent(FilterSet): f1 = CharFilter() f2 = CharFilter() class Child(Parent): f1 = None class Grandchild(Child): pass self.assertEqual(len(Parent.base_filters), 2) self.assertEqual(len(Child.base_filters), 1) self.assertEqual(len(Grandchild.base_filters), 1) @override_settings(FILTERS_DEFAULT_LOOKUP_EXPR="lt") def test_transforms_other_default_lookup(self): class F(FilterSet): class Meta: model = Article fields = { "published": ["lt", "year__lt"], } self.assertEqual(len(F.base_filters), 2) expected_list = ["published", "published__year"] self.assertCountEqual(list(F.base_filters), expected_list) def test_declared_filter_multiple_inheritance(self): class A(FilterSet): f = CharFilter() class B(FilterSet): f = NumberFilter() class F(A, B): pass filters = {name: type(f) for name, f in F.declared_filters.items()} self.assertEqual(filters, {"f": CharFilter}) def test_declared_filter_multiple_inheritance_field_ordering(self): class Base(FilterSet): f1 = CharFilter() f2 = CharFilter() class A(Base): f3 = NumberFilter() class B(FilterSet): f3 = CharFilter() f4 = CharFilter() class F(A, B): f2 = NumberFilter() f5 = CharFilter() fields = {name: type(f) for name, f in F.declared_filters.items()} # `NumberFilter`s should be the 'winners' in filter name conflicts # - `F.f2` should override `Base.F2` # - `A.f3` should override `B.f3` assert fields == { "f1": CharFilter, "f2": NumberFilter, "f3": NumberFilter, "f4": CharFilter, "f5": CharFilter, } class FilterSetInstantiationTests(TestCase): class F(FilterSet): class Meta: model = User fields = ["username"] def test_creating_instance(self): f = self.F() self.assertFalse(f.is_bound) self.assertIsNotNone(f.queryset) self.assertEqual(len(f.filters), len(self.F.base_filters)) for name, filter_ in f.filters.items(): self.assertEqual( filter_.model, User, "%s does not have model set correctly" % name ) def test_creating_bound_instance(self): f = self.F({"username": "username"}) self.assertTrue(f.is_bound) def test_creating_with_queryset(self): m = mock.Mock() f = self.F(queryset=m) self.assertEqual(f.queryset, m) def test_creating_with_request(self): m = mock.Mock() f = self.F(request=m) self.assertEqual(f.request, m) class FilterSetQuerysetTests(TestCase): class F(FilterSet): invalid = CharFilter(method=lambda *args: None) class Meta: model = User fields = ["username", "invalid"] def test_filter_queryset_called_once(self): m = MockQuerySet() f = self.F({"username": "bob"}, queryset=m) with mock.patch.object(f, "filter_queryset", wraps=f.filter_queryset) as fn: f.qs fn.assert_called_once_with(m.all()) f.qs fn.assert_called_once_with(m.all()) def test_get_form_class_called_once(self): f = self.F() with mock.patch.object(f, "get_form_class", wraps=f.get_form_class) as fn: f.form fn.assert_called_once() f.form fn.assert_called_once() def test_qs_caching(self): m = mock.Mock() f = self.F(queryset=m) self.assertIs(f.qs, m.all()) self.assertIs(f.qs, f.qs) def test_form_caching(self): f = self.F() self.assertIs(f.form, f.form) def test_qs_triggers_form_validation(self): m = MockQuerySet() f = self.F({"username": "bob"}, queryset=m) with mock.patch.object(f.form, "full_clean", wraps=f.form.full_clean) as fn: fn.assert_not_called() f.qs fn.assert_called() def test_filters_must_return_queryset(self): m = MockQuerySet() f = self.F({"invalid": "result"}, queryset=m) msg = "Expected 'F.invalid' to return a QuerySet, but got a NoneType instead." with self.assertRaisesMessage(AssertionError, msg): f.qs # test filter.method here, as it depends on its parent FilterSet class FilterMethodTests(TestCase): def test_none(self): # use a mock to bypass bound/unbound method equality class TestFilter(Filter): filter = mock.Mock() f = TestFilter(method=None) self.assertIsNone(f.method) # passing method=None should not modify filter function self.assertIs(f.filter, TestFilter.filter) def test_method_name(self): class F(FilterSet): f = Filter(method="filter_f") def filter_f(self, qs, name, value): pass f = F({}, queryset=User.objects.all()) self.assertEqual(f.filters["f"].method, "filter_f") self.assertEqual(f.filters["f"].filter.method, f.filter_f) self.assertIsInstance(f.filters["f"].filter, FilterMethod) def test_method_callable(self): def filter_f(qs, name, value): pass class F(FilterSet): f = Filter(method=filter_f) f = F({}, queryset=User.objects.all()) self.assertEqual(f.filters["f"].method, filter_f) self.assertEqual(f.filters["f"].filter.method, filter_f) self.assertIsInstance(f.filters["f"].filter, FilterMethod) def test_request_available_during_method_called(self): class F(FilterSet): f = Filter(method="filter_f") def filter_f(self, qs, name, value): # call mock request object to prove self.request can be accessed self.request() m = mock.Mock() f = F({}, queryset=User.objects.all(), request=m) # call the filter f.filters["f"].filter.method(User.objects.all(), "f", "") m.assert_called_once_with() def test_method_with_overridden_filter(self): # Some filter classes override the base filter() method. We need # to ensure that passing a method argument still works correctly class F(FilterSet): f = DateRangeFilter(method="filter_f") def filter_f(self, qs, name, value): pass f = F({}, queryset=User.objects.all()) self.assertEqual(f.filters["f"].method, "filter_f") self.assertEqual(f.filters["f"].filter.method, f.filter_f) def test_parent_unresolvable(self): f = Filter(method="filter_f") with self.assertRaises(AssertionError) as w: f.filter(User.objects.all(), 0) self.assertIn("'None'", str(w.exception)) self.assertIn("parent", str(w.exception)) self.assertIn("filter_f", str(w.exception)) def test_method_self_is_parent(self): # Ensure the method isn't 're-parented' on the `FilterMethod` helper class. # Filter methods should have access to the filterset's properties. request = MockQuerySet() class F(FilterSet): f = CharFilter(method="filter_f") class Meta: model = User fields = [] def filter_f(inner_self, qs, name, value): self.assertIsInstance(inner_self, F) self.assertIs(inner_self.request, request) return qs F({"f": "foo"}, request=request, queryset=User.objects.all()).qs def test_method_unresolvable(self): class F(FilterSet): f = Filter(method="filter_f") f = F({}, queryset=User.objects.all()) with self.assertRaises(AssertionError) as w: f.filters["f"].filter(User.objects.all(), 0) self.assertIn("%s.%s" % (F.__module__, F.__name__), str(w.exception)) self.assertIn(".filter_f()", str(w.exception)) def test_method_uncallable(self): class F(FilterSet): f = Filter(method="filter_f") filter_f = 4 f = F({}, queryset=User.objects.all()) with self.assertRaises(AssertionError) as w: f.filters["f"].filter(User.objects.all(), 0) self.assertIn("%s.%s" % (F.__module__, F.__name__), str(w.exception)) self.assertIn(".filter_f()", str(w.exception)) def test_method_set_unset(self): # use a mock to bypass bound/unbound method equality class TestFilter(Filter): filter = mock.Mock() f = TestFilter(method="filter_f") self.assertEqual(f.method, "filter_f") self.assertIsInstance(f.filter, FilterMethod) # setting None should revert to Filter.filter f.method = None self.assertIsNone(f.method) self.assertIs(f.filter, TestFilter.filter) class MiscFilterSetTests(TestCase): def test_no__getitem__(self): # The DTL processes variable lookups by the following rules: # https://docs.djangoproject.com/en/stable/ref/templates/language/#variables # A __getitem__ implementation precedes normal attribute access, and in # the case of #58, will force the queryset to evaluate when it should # not (eg, when rendering a blank form). self.assertFalse(hasattr(FilterSet, "__getitem__")) def test_no_qs_proxying(self): # The FilterSet should not proxy .qs methods - just access .qs directly self.assertFalse(hasattr(FilterSet, "__len__")) self.assertFalse(hasattr(FilterSet, "__iter__")) django-filter-23.5/tests/test_fields.py0000644000175000017500000002443514533556010020103 0ustar carstencarstenimport datetime as dt import decimal import unittest from datetime import datetime, time, timedelta, tzinfo import django import pytz from django import forms from django.test import TestCase, override_settings from django.utils import timezone from django_filters.fields import ( BaseCSVField, BaseRangeField, DateRangeField, DateTimeRangeField, IsoDateTimeField, IsoDateTimeRangeField, Lookup, LookupChoiceField, RangeField, TimeRangeField, ) from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget def to_d(float_value): return decimal.Decimal("%.2f" % float_value) class LookupTests(TestCase): def test_empty_attrs(self): with self.assertRaisesMessage(ValueError, ""): Lookup(None, None) with self.assertRaisesMessage(ValueError, ""): Lookup("", "") def test_empty_value(self): with self.assertRaisesMessage(ValueError, ""): Lookup("", "exact") def test_empty_lookup_expr(self): with self.assertRaisesMessage(ValueError, ""): Lookup("Value", "") class RangeFieldTests(TestCase): def test_field(self): f = RangeField() self.assertEqual(len(f.fields), 2) def test_clean(self): w = RangeWidget() f = RangeField(widget=w, required=False) self.assertEqual(f.clean(["12.34", "55"]), slice(to_d(12.34), to_d(55))) self.assertIsNone(f.clean([])) class DateRangeFieldTests(TestCase): def test_field(self): f = DateRangeField() self.assertEqual(len(f.fields), 2) @override_settings(USE_TZ=False) def test_clean(self): w = RangeWidget() f = DateRangeField(widget=w, required=False) self.assertEqual( f.clean(["2015-01-01", "2015-01-10"]), slice( datetime(2015, 1, 1, 0, 0, 0), datetime(2015, 1, 10, 23, 59, 59, 999999) ), ) self.assertIsNone(f.clean([])) class DateTimeRangeFieldTests(TestCase): def test_field(self): f = DateTimeRangeField() self.assertEqual(len(f.fields), 2) @override_settings(USE_TZ=False) def test_clean(self): w = RangeWidget() f = DateTimeRangeField(widget=w) self.assertEqual( f.clean(["2015-01-01 10:30", "2015-01-10 8:45"]), slice(datetime(2015, 1, 1, 10, 30, 0), datetime(2015, 1, 10, 8, 45, 0)), ) class IsoDateTimeRangeFieldTests(TestCase): def test_field(self): f = IsoDateTimeRangeField() self.assertEqual(len(f.fields), 2) def test_clean(self): w = RangeWidget() f = IsoDateTimeRangeField(widget=w) expected = slice( datetime(2015, 1, 1, 9, 30, 1, 123000, tzinfo=dt.timezone.utc), datetime(2015, 1, 10, 7, 45, 2, 345000, tzinfo=dt.timezone.utc), ) actual = f.clean( ["2015-01-01T10:30:01.123000+01:00", "2015-01-10T08:45:02.345000+01:00"] ) self.assertEqual(expected, actual) class TimeRangeFieldTests(TestCase): def test_field(self): f = DateRangeField() self.assertEqual(len(f.fields), 2) def test_clean(self): w = RangeWidget() f = TimeRangeField(widget=w) self.assertEqual( f.clean(["10:15", "12:30"]), slice(time(10, 15, 0), time(12, 30, 0)) ) class LookupChoiceFieldTests(TestCase): def test_field(self): inner = forms.DecimalField() f = LookupChoiceField(inner, [("gt", "gt"), ("lt", "lt")]) self.assertEqual(len(f.fields), 2) def test_clean(self): inner = forms.DecimalField() f = LookupChoiceField(inner, [("gt", "gt"), ("lt", "lt")], required=False) self.assertEqual(f.clean(["12.34", "lt"]), Lookup(to_d(12.34), "lt")) self.assertEqual(f.clean([]), None) with self.assertRaisesMessage(forms.ValidationError, "Select a lookup."): f.clean(["12.34", ""]) def test_render_used_html5(self): inner = forms.DecimalField() f = LookupChoiceField(inner, [("gt", "gt"), ("lt", "lt")], empty_label=None) self.assertHTMLEqual( f.widget.render("price", ""), """ """, ) self.assertHTMLEqual( f.widget.render("price", ["abc", "lt"]), """ """, ) class IsoDateTimeFieldTests(TestCase): reference_str = "2015-07-19T13:34:51.759" reference_dt = datetime(2015, 7, 19, 13, 34, 51, 759000) field = IsoDateTimeField() def parse_input(self, value): return self.field.strptime(value, IsoDateTimeField.ISO_8601) def test_datetime_string_is_parsed(self): d = self.parse_input(self.reference_str) self.assertTrue(isinstance(d, datetime)) def test_datetime_string_with_timezone_is_parsed(self): d = self.parse_input(self.reference_str + "+01:00") self.assertTrue(isinstance(d, datetime)) def test_datetime_zulu(self): d = self.parse_input(self.reference_str + "Z") self.assertTrue(isinstance(d, datetime)) @unittest.skipUnless(django.VERSION < (5, 0), "pytz support removed in Django 5.0") @override_settings(TIME_ZONE="UTC") def test_datetime_timezone_awareness(self): utc, tokyo = pytz.timezone("UTC"), pytz.timezone("Asia/Tokyo") # by default, use the server timezone reference = utc.localize(self.reference_dt) parsed = self.parse_input(self.reference_str) self.assertIsInstance(parsed.tzinfo, tzinfo) self.assertEqual(parsed, reference) # if set, use the active timezone reference = tokyo.localize(self.reference_dt) with timezone.override(tokyo): parsed = self.parse_input(self.reference_str) self.assertIsInstance(parsed.tzinfo, tzinfo) self.assertEqual(parsed.tzinfo.zone, tokyo.zone) self.assertEqual(parsed, reference) # if provided, utc offset should have precedence reference = utc.localize(self.reference_dt - timedelta(hours=1)) parsed = self.parse_input(self.reference_str + "+01:00") self.assertIsInstance(parsed.tzinfo, tzinfo) self.assertEqual(parsed, reference) @override_settings(USE_TZ=False) def test_datetime_timezone_naivety(self): reference = self.reference_dt.replace() parsed = self.parse_input(self.reference_str + "+01:00") self.assertIsNone(parsed.tzinfo) self.assertEqual(parsed, reference - timedelta(hours=1)) parsed = self.parse_input(self.reference_str) self.assertIsNone(parsed.tzinfo) self.assertEqual(parsed, reference) def test_datetime_non_iso_format(self): f = IsoDateTimeField() parsed = f.strptime("19-07-2015T51:34:13.759", "%d-%m-%YT%S:%M:%H.%f") self.assertTrue(isinstance(parsed, datetime)) self.assertEqual(parsed, self.reference_dt) def test_datetime_wrong_format(self): with self.assertRaises(ValueError): self.parse_input("19-07-2015T51:34:13.759") class BaseCSVFieldTests(TestCase): class DecimalCSVField(BaseCSVField, forms.DecimalField): pass def test_clean(self): # Filter class sets required=False by default field = self.DecimalCSVField(required=False) self.assertEqual(field.clean(None), None) self.assertEqual(field.clean(""), []) self.assertEqual(field.clean(["1"]), [1]) self.assertEqual(field.clean(["1", "2"]), [1, 2]) self.assertEqual(field.clean(["1", "2", "3"]), [1, 2, 3]) def test_validation_error(self): field = self.DecimalCSVField() msg = "Enter a number." with self.assertRaisesMessage(forms.ValidationError, msg): field.clean(["a", "b", "c"]) def test_required_error(self): field = self.DecimalCSVField(required=True) msg = "This field is required." with self.assertRaisesMessage(forms.ValidationError, msg): field.clean(None) with self.assertRaisesMessage(forms.ValidationError, msg): field.clean([""]) def test_derived_widget(self): with self.assertRaises(AssertionError) as excinfo: BaseCSVField(widget=RangeWidget()) msg = str(excinfo.exception) self.assertIn("'BaseCSVField.widget' must be a widget class", msg) self.assertIn("RangeWidget", msg) widget = CSVWidget(attrs={"class": "class"}) field = BaseCSVField(widget=widget) self.assertIsInstance(field.widget, CSVWidget) self.assertEqual(field.widget.attrs, {"class": "class"}) field = BaseCSVField(widget=CSVWidget) self.assertIsInstance(field.widget, CSVWidget) field = BaseCSVField(widget=forms.Select) self.assertIsInstance(field.widget, forms.Select) self.assertIsInstance(field.widget, BaseCSVWidget) class BaseRangeFieldTests(TestCase): class DecimalRangeField(BaseRangeField, forms.DecimalField): pass def test_clean(self): # Filter class sets required=False by default field = self.DecimalRangeField(required=False) self.assertEqual(field.clean(None), None) self.assertEqual(field.clean(""), []) self.assertEqual(field.clean([]), []) self.assertEqual(field.clean(["1", "2"]), [1, 2]) def test_validation_error(self): field = self.DecimalRangeField() msg = "Range query expects two values." with self.assertRaisesMessage(forms.ValidationError, msg): field.clean(["1"]) with self.assertRaisesMessage(forms.ValidationError, msg): field.clean(["1", "2", "3"]) def test_required_error(self): field = self.DecimalRangeField(required=True) msg = "This field is required." with self.assertRaisesMessage(forms.ValidationError, msg): field.clean(None) with self.assertRaisesMessage(forms.ValidationError, msg): field.clean([""]) django-filter-23.5/tests/test_conf.py0000644000175000017500000000654514533556010017564 0ustar carstencarstenfrom unittest import mock from django.test import TestCase, override_settings from django_filters.conf import is_callable, settings class DefaultSettingsTests(TestCase): def test_verbose_lookups(self): self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict) self.assertIn("exact", settings.VERBOSE_LOOKUPS) def test_default_lookup_expr(self): self.assertEqual(settings.DEFAULT_LOOKUP_EXPR, "exact") def test_disable_help_text(self): self.assertFalse(settings.DISABLE_HELP_TEXT) def test_empty_choice_label(self): self.assertEqual(settings.EMPTY_CHOICE_LABEL, "---------") def test_null_choice_label(self): self.assertIsNone(settings.NULL_CHOICE_LABEL) def test_null_choice_value(self): self.assertEqual(settings.NULL_CHOICE_VALUE, "null") class OverrideSettingsTests(TestCase): def test_attribute_override(self): self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict) original = settings.VERBOSE_LOOKUPS with override_settings(FILTERS_VERBOSE_LOOKUPS=None): self.assertIsNone(settings.VERBOSE_LOOKUPS) self.assertIs(settings.VERBOSE_LOOKUPS, original) def test_missing_attribute_override(self): # ensure that changed setting behaves correctly when # not originally present in the user's settings. from django.conf import settings as dj_settings self.assertFalse(hasattr(dj_settings, "FILTERS_DISABLE_HELP_TEXT")) # Default value self.assertFalse(settings.DISABLE_HELP_TEXT) with override_settings(FILTERS_DISABLE_HELP_TEXT=True): self.assertTrue(settings.DISABLE_HELP_TEXT) # Revert to default self.assertFalse(settings.DISABLE_HELP_TEXT) def test_non_filters_setting(self): self.assertFalse(hasattr(settings, "USE_TZ")) with override_settings(USE_TZ=False): self.assertFalse(hasattr(settings, "USE_TZ")) self.assertFalse(hasattr(settings, "USE_TZ")) def test_non_existent_setting(self): self.assertFalse(hasattr(settings, "FILTERS_FOOBAR")) self.assertFalse(hasattr(settings, "FOOBAR")) with override_settings(FILTERS_FOOBAR="blah"): self.assertFalse(hasattr(settings, "FILTERS_FOOBAR")) self.assertFalse(hasattr(settings, "FOOBAR")) self.assertFalse(hasattr(settings, "FILTERS_FOOBAR")) self.assertFalse(hasattr(settings, "FOOBAR")) class IsCallableTests(TestCase): def test_behavior(self): def func(): pass class Class: def __call__(self): pass def method(self): pass c = Class() self.assertTrue(is_callable(func)) self.assertFalse(is_callable(Class)) self.assertTrue(is_callable(c)) self.assertTrue(is_callable(c.method)) class SettingsObjectTestCase(TestCase): @mock.patch("django_filters.conf.DEPRECATED_SETTINGS", ["TEST_123"]) @mock.patch.dict("django_filters.conf.DEFAULTS", {"TEST_123": True}) def test_get_setting_deprecated(self): with override_settings(FILTERS_TEST_123=True): with self.assertWarns(DeprecationWarning): settings.change_setting("FILTERS_TEST_123", True, True) test_setting = settings.get_setting("TEST_123") self.assertTrue(test_setting) django-filter-23.5/tests/__init__.py0000644000175000017500000000000014533556010017313 0ustar carstencarstendjango-filter-23.5/tests/templates/0000755000175000017500000000000014533556010017212 5ustar carstencarstendjango-filter-23.5/tests/templates/tests/0000755000175000017500000000000014533556010020354 5ustar carstencarstendjango-filter-23.5/tests/templates/tests/book_filter.html0000644000175000017500000000011314533556010023534 0ustar carstencarsten{{ filter.form }} {% for obj in object_list %} {{ obj }} {% endfor %} django-filter-23.5/tests/models.py0000644000175000017500000001234314533556010017054 0ustar carstencarstenfrom django import forms from django.db import models from django.utils.translation import gettext_lazy as _ REGULAR = 0 MANAGER = 1 ADMIN = 2 STATUS_CHOICES = ( (REGULAR, "Regular"), (MANAGER, "Manager"), (ADMIN, "Admin"), ) # classes for testing filters with inherited fields class SubCharField(models.CharField): pass class SubSubCharField(SubCharField): pass class SubnetMaskField(models.Field): empty_strings_allowed = False description = "Subnet Mask" def __init__(self, *args, **kwargs): kwargs["max_length"] = 15 models.Field.__init__(self, *args, **kwargs) def get_internal_type(self): return "GenericIPAddressField" def formfield(self, **kwargs): defaults = {"form_class": forms.GenericIPAddressField} defaults.update(kwargs) return super().formfield(**defaults) class User(models.Model): username = models.CharField(_("username"), max_length=255) first_name = SubCharField(max_length=100) last_name = SubSubCharField(max_length=100) status = models.IntegerField(choices=STATUS_CHOICES, default=0) is_active = models.BooleanField(default=False) is_employed = models.BooleanField(null=True, default=False) favorite_books = models.ManyToManyField("Book", related_name="lovers") def __str__(self): return self.username class ManagerGroup(models.Model): users = models.ManyToManyField( User, limit_choices_to={"is_active": True}, related_name="member_of" ) manager = models.ForeignKey( User, limit_choices_to=lambda: {"status": MANAGER}, related_name="manager_of", on_delete=models.CASCADE, ) def __str__(self): return self.manager.name + " group" class AdminUser(User): class Meta: proxy = True def __str__(self): return "%s (ADMIN)" % self.username class Comment(models.Model): text = models.TextField() author = models.ForeignKey(User, related_name="comments", on_delete=models.CASCADE) date = models.DateField() time = models.TimeField() def __str__(self): return "%s said %s" % (self.author, self.text[:25]) class Article(models.Model): name = models.CharField(verbose_name="title", max_length=200, blank=True) published = models.DateTimeField() author = models.ForeignKey(User, null=True, on_delete=models.CASCADE) def __str__(self): if self.author_id: return "%s on %s" % (self.author, self.published) return "Anonymous on %s" % self.published class Book(models.Model): title = models.CharField(max_length=100) price = models.DecimalField(max_digits=6, decimal_places=2) average_rating = models.FloatField() def __str__(self): return self.title class Place(models.Model): name = models.CharField(max_length=100) class Meta: abstract = True class Restaurant(Place): serves_pizza = models.BooleanField(default=False) class NetworkSetting(models.Model): ip = models.GenericIPAddressField() mask = SubnetMaskField() cidr = models.CharField(max_length=18, blank=True, verbose_name="CIDR") class Company(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Meta: ordering = ["name"] class Location(models.Model): company = models.ForeignKey( Company, related_name="locations", on_delete=models.CASCADE ) name = models.CharField(max_length=100) zip_code = models.CharField(max_length=10) open_days = models.CharField(max_length=7) def __str__(self): return "%s: %s" % (self.company.name, self.name) class Account(models.Model): name = models.CharField(max_length=100) in_good_standing = models.BooleanField(default=False) friendly = models.BooleanField(default=False) class Profile(models.Model): account = models.OneToOneField( Account, related_name="profile", on_delete=models.CASCADE ) likes_coffee = models.BooleanField(default=False) likes_tea = models.BooleanField(default=False) class BankAccount(Account): amount_saved = models.IntegerField(default=0) class Node(models.Model): name = models.CharField(max_length=20) adjacents = models.ManyToManyField("self") class DirectedNode(models.Model): name = models.CharField(max_length=20) outbound_nodes = models.ManyToManyField( "self", symmetrical=False, related_name="inbound_nodes" ) class Worker(models.Model): name = models.CharField(max_length=100) class HiredWorker(models.Model): salary = models.IntegerField() hired_on = models.DateField() worker = models.ForeignKey(Worker, on_delete=models.CASCADE) business = models.ForeignKey("Business", on_delete=models.CASCADE) class Business(models.Model): name = models.CharField(max_length=100) employees = models.ManyToManyField( Worker, through=HiredWorker, related_name="employers" ) class UUIDTestModel(models.Model): uuid = models.UUIDField() class SpacewalkRecord(models.Model): """Cumulative space walk record. See: https://en.wikipedia.org/wiki/List_of_cumulative_spacewalk_records """ astronaut = models.CharField(max_length=100) duration = models.DurationField() django-filter-23.5/tests/urls.py0000644000175000017500000000054214533556010016554 0ustar carstencarstenfrom django.urls import path from django_filters.views import FilterView, object_filter from .models import Book def _foo(): return "bar" urlpatterns = [ path( "books-legacy/", object_filter, {"model": Book, "extra_context": {"foo": _foo, "bar": "foo"}}, ), path("books/", FilterView.as_view(model=Book)), ] django-filter-23.5/tests/test_utils.py0000644000175000017500000005001114533556010017762 0ustar carstencarstenimport datetime import unittest import warnings import django from django.db import models from django.db.models.constants import LOOKUP_SEP from django.db.models.fields.related import ForeignObjectRel from django.test import TestCase, override_settings from django.utils.functional import Promise from django.utils.timezone import get_default_timezone, make_aware from django_filters import FilterSet from django_filters.exceptions import FieldLookupError from django_filters.filters import MultipleChoiceFilter from django_filters.utils import ( MigrationNotice, RenameAttributesBase, get_field_parts, get_model_field, handle_timezone, label_for_filter, resolve_field, translate_validation, verbose_field_name, verbose_lookup_expr, ) from .models import Article, Book, Business, Company, HiredWorker, NetworkSetting, User class MigrationNoticeTests(TestCase): def test_message(self): self.assertEqual( str(MigrationNotice("Message.")), "Message. See: https://django-filter.readthedocs.io/en/main/guide/migration.html", ) class RenameAttributes(RenameAttributesBase): renamed_attributes = (("old", "new", DeprecationWarning),) class SENTINEL: pass class RenameAttributesBaseTests(TestCase): def check(self, recorded, count): expected = "`Example.old` attribute should be renamed `new`." self.assertEqual(len(recorded), count) for _ in range(count): message = str(recorded.pop().message) self.assertEqual(message, expected) self.assertEqual(len(recorded), 0) def test_class_creation_warnings(self): with warnings.catch_warnings(record=True) as recorded: warnings.simplefilter("always") class Example(metaclass=RenameAttributes): old = SENTINEL # single warning for renamed attr on creation self.check(recorded, 1) def test_renamed_attribute_in_class_dict(self): with warnings.catch_warnings(record=True) as recorded: warnings.simplefilter("ignore") class Example(metaclass=RenameAttributes): old = SENTINEL warnings.simplefilter("always") # Ensure `old` and `new` are not both in class dict. self.assertNotIn("old", Example.__dict__) self.assertIn("new", Example.__dict__) # Ensure `old` value assigned to `new`. self.assertEqual(Example.new, SENTINEL) self.check(recorded, 0) def test_class_accessor_warnings(self): with warnings.catch_warnings(record=True) as recorded: warnings.simplefilter("ignore") class Example(metaclass=RenameAttributes): new = None warnings.simplefilter("always") self.assertIsNone(Example.new) self.assertIsNone(Example.old) self.check(recorded, 1) Example.old = SENTINEL self.assertIs(Example.new, SENTINEL) self.assertIs(Example.old, SENTINEL) self.check(recorded, 2) def test_instance_accessor_warnings(self): with warnings.catch_warnings(record=True) as recorded: warnings.simplefilter("ignore") class Example(metaclass=RenameAttributes): new = None warnings.simplefilter("always") example = Example() self.check(recorded, 0) self.assertIsNone(example.new) self.assertIsNone(example.old) self.check(recorded, 1) example.old = SENTINEL self.assertIs(example.new, SENTINEL) self.assertIs(example.old, SENTINEL) self.check(recorded, 2) def test_class_instance_values(self): with warnings.catch_warnings(record=True): warnings.simplefilter("ignore") class Example(metaclass=RenameAttributes): new = None example = Example() # setting instance should not affect class example.old = SENTINEL self.assertIsNone(Example.old) self.assertIsNone(Example.new) self.assertIs(example.old, SENTINEL) self.assertIs(example.new, SENTINEL) def test_getter_reachable(self): with warnings.catch_warnings(record=True) as recorded: warnings.simplefilter("always") class Example(metaclass=RenameAttributes): def __getattr__(self, name): if name == "test": return SENTINEL return self.__getattribute__(name) example = Example() self.assertIs(example.test, SENTINEL) self.check(recorded, 0) def test_parent_getter_reachable(self): with warnings.catch_warnings(record=True) as recorded: warnings.simplefilter("always") class Parent: def __getattr__(self, name): if name == "test": return SENTINEL return self.__getattribute__(name) class Example(Parent, metaclass=RenameAttributes): pass example = Example() self.assertIs(example.test, SENTINEL) self.check(recorded, 0) def test_setter_reachable(self): with warnings.catch_warnings(record=True) as recorded: warnings.simplefilter("always") class Example(metaclass=RenameAttributes): def __setattr__(self, name, value): if name == "test": value = SENTINEL super().__setattr__(name, value) example = Example() example.test = None self.assertIs(example.test, SENTINEL) self.check(recorded, 0) class GetFieldPartsTests(TestCase): def test_field(self): parts = get_field_parts(User, "username") self.assertEqual(len(parts), 1) self.assertIsInstance(parts[0], models.CharField) def test_non_existent_field(self): result = get_model_field(User, "unknown__name") self.assertIsNone(result) def test_forwards_related_field(self): parts = get_field_parts(User, "favorite_books__title") self.assertEqual(len(parts), 2) self.assertIsInstance(parts[0], models.ManyToManyField) self.assertIsInstance(parts[1], models.CharField) def test_reverse_related_field(self): parts = get_field_parts(User, "manager_of__users__username") self.assertEqual(len(parts), 3) self.assertIsInstance(parts[0], ForeignObjectRel) self.assertIsInstance(parts[1], models.ManyToManyField) self.assertIsInstance(parts[2], models.CharField) def test_lazy_relationship_not_ready(self): """ This simulates trying to create a FilterSet before the app registry has been populated. Lazy relationships have not yet been resolved from their strings into their remote model references. """ class TestModel(models.Model): fk = models.ForeignKey("remote.Model", on_delete=models.CASCADE) msg = ( "Unable to resolve relationship `fk__f` for `tests.TestModel`. " "Django is most likely not initialized, and its apps registry " "not populated. Ensure Django has finished setup before loading " "`FilterSet`s." ) with self.assertRaisesMessage(RuntimeError, msg): get_field_parts(TestModel, "fk__f") class GetModelFieldTests(TestCase): def test_non_existent_field(self): result = get_model_field(User, "unknown__name") self.assertIsNone(result) def test_related_field(self): result = get_model_field(Business, "hiredworker__worker") self.assertEqual(result, HiredWorker._meta.get_field("worker")) class ResolveFieldTests(TestCase): def test_resolve_plain_lookups(self): """ Check that the standard query terms can be correctly resolved. eg, an 'EXACT' lookup on a user's username """ model_field = User._meta.get_field("username") lookups = model_field.class_lookups.keys() # This is simple - the final output of an untransformed field is itself. # The lookups are the default lookups registered to the class. for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.CharField) self.assertEqual(lookup, term) def test_resolve_forward_related_lookups(self): """ Check that lookups can be resolved for related fields in the forwards direction. """ lookups = [ "exact", "gte", "gt", "lte", "lt", "in", "isnull", ] # ForeignKey model_field = Article._meta.get_field("author") for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ForeignKey) self.assertEqual(lookup, term) # ManyToManyField model_field = User._meta.get_field("favorite_books") for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToManyField) self.assertEqual(lookup, term) def test_resolve_reverse_related_lookups(self): """ Check that lookups can be resolved for related fields in the reverse direction. """ lookups = [ "exact", "gte", "gt", "lte", "lt", "in", "isnull", ] # ManyToOneRel model_field = User._meta.get_field("article") for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToOneRel) self.assertEqual(lookup, term) # ManyToManyRel model_field = Book._meta.get_field("lovers") for term in lookups: field, lookup = resolve_field(model_field, term) self.assertIsInstance(field, models.ManyToManyRel) self.assertEqual(lookup, term) def test_resolve_transformed_lookups(self): """ Check that chained field transforms are correctly resolved. eg, a 'date__year__gte' lookup on an article's 'published' timestamp. """ # Use a DateTimeField, so we can check multiple transforms. # eg, date__year__gte model_field = Article._meta.get_field("published") standard_lookups = [ "exact", "iexact", "gte", "gt", "lte", "lt", ] date_lookups = [ "year", "month", "day", "week_day", ] datetime_lookups = date_lookups + [ "hour", "minute", "second", ] # ex: 'date__gt' for lookup in standard_lookups: field, resolved_lookup = resolve_field( model_field, LOOKUP_SEP.join(["date", lookup]) ) self.assertIsInstance(field, models.DateField) self.assertEqual(resolved_lookup, lookup) # ex: 'year__iexact' for part in datetime_lookups: for lookup in standard_lookups: field, resolved_lookup = resolve_field( model_field, LOOKUP_SEP.join([part, lookup]) ) self.assertIsInstance(field, models.IntegerField) self.assertEqual(resolved_lookup, lookup) # ex: 'date__year__lte' for part in date_lookups: for lookup in standard_lookups: field, resolved_lookup = resolve_field( model_field, LOOKUP_SEP.join(["date", part, lookup]) ) self.assertIsInstance(field, models.IntegerField) self.assertEqual(resolved_lookup, lookup) def test_resolve_implicit_exact_lookup(self): # Use a DateTimeField, so we can check multiple transforms. # eg, date__year__gte model_field = Article._meta.get_field("published") field, lookup = resolve_field(model_field, "date") self.assertIsInstance(field, models.DateField) self.assertEqual(lookup, "exact") field, lookup = resolve_field(model_field, "date__year") self.assertIsInstance(field, models.IntegerField) self.assertEqual(lookup, "exact") def test_invalid_lookup_expression(self): model_field = Article._meta.get_field("published") with self.assertRaises(FieldLookupError) as context: resolve_field(model_field, "invalid_lookup") exc = str(context.exception) self.assertIn(str(model_field), exc) self.assertIn("invalid_lookup", exc) def test_invalid_transformed_lookup_expression(self): model_field = Article._meta.get_field("published") with self.assertRaises(FieldLookupError) as context: resolve_field(model_field, "date__invalid_lookup") exc = str(context.exception) self.assertIn(str(model_field), exc) self.assertIn("date__invalid_lookup", exc) class VerboseFieldNameTests(TestCase): def test_none(self): verbose_name = verbose_field_name(Article, None) self.assertEqual(verbose_name, "[invalid name]") def test_invalid_name(self): verbose_name = verbose_field_name(Article, "foobar") self.assertEqual(verbose_name, "[invalid name]") def test_field(self): verbose_name = verbose_field_name(Article, "author") self.assertEqual(verbose_name, "author") def test_field_with_verbose_name(self): verbose_name = verbose_field_name(Article, "name") self.assertEqual(verbose_name, "title") def test_field_all_caps(self): verbose_name = verbose_field_name(NetworkSetting, "cidr") self.assertEqual(verbose_name, "CIDR") def test_forwards_related_field(self): verbose_name = verbose_field_name(Article, "author__username") self.assertEqual(verbose_name, "author username") def test_backwards_related_field(self): verbose_name = verbose_field_name(Book, "lovers__first_name") self.assertEqual(verbose_name, "lovers first name") def test_backwards_related_field_multi_word(self): verbose_name = verbose_field_name(User, "manager_of") self.assertEqual(verbose_name, "manager of") def test_lazy_text(self): # sanity check field = User._meta.get_field("username") self.assertIsInstance(field.verbose_name, Promise) verbose_name = verbose_field_name(User, "username") self.assertEqual(verbose_name, "username") def test_forwards_fk(self): verbose_name = verbose_field_name(Article, "author") self.assertEqual(verbose_name, "author") def test_backwards_fk(self): # https://github.com/carltongibson/django-filter/issues/716 # related_name is set verbose_name = verbose_field_name(Company, "locations") self.assertEqual(verbose_name, "locations") # related_name not set. Auto-generated relation is `article_set` # _meta.get_field raises FieldDoesNotExist verbose_name = verbose_field_name(User, "article_set") self.assertEqual(verbose_name, "[invalid name]") # WRONG NAME! Returns ManyToOneRel with related_name == None. verbose_name = verbose_field_name(User, "article") self.assertEqual(verbose_name, "[invalid name]") class VerboseLookupExprTests(TestCase): def test_exact(self): # Exact should default to empty. A verbose expression is unnecessary, # and this behavior works well with list syntax for `Meta.fields`. verbose_lookup = verbose_lookup_expr("exact") self.assertEqual(verbose_lookup, "") def test_verbose_expression(self): verbose_lookup = verbose_lookup_expr("date__lt") self.assertEqual(verbose_lookup, "date is less than") def test_missing_keys(self): verbose_lookup = verbose_lookup_expr("foo__bar__lt") self.assertEqual(verbose_lookup, "foo bar is less than") @override_settings(FILTERS_VERBOSE_LOOKUPS={"exact": "is equal to"}) def test_overridden_settings(self): verbose_lookup = verbose_lookup_expr("exact") self.assertEqual(verbose_lookup, "is equal to") class LabelForFilterTests(TestCase): def test_standard_label(self): label = label_for_filter(Article, "name", "in") self.assertEqual(label, "Title is in") def test_related_model(self): label = label_for_filter(Article, "author__first_name", "in") self.assertEqual(label, "Author first name is in") def test_exclusion_label(self): label = label_for_filter(Article, "name", "in", exclude=True) self.assertEqual(label, "Exclude title is in") def test_related_model_exclusion(self): label = label_for_filter(Article, "author__first_name", "in", exclude=True) self.assertEqual(label, "Exclude author first name is in") def test_exact_lookup(self): label = label_for_filter(Article, "name", "exact") self.assertEqual(label, "Title") def test_field_all_caps(self): label = label_for_filter(NetworkSetting, "cidr", "contains", exclude=True) self.assertEqual(label, "Exclude CIDR contains") @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") class HandleTimezone(TestCase): @override_settings(TIME_ZONE="America/Sao_Paulo") def test_handle_dst_ending(self): dst_ending_date = datetime.datetime(2017, 2, 18, 23, 59, 59, 999999) handled = handle_timezone(dst_ending_date, False) self.assertEqual( handled, make_aware(dst_ending_date, get_default_timezone(), False), ) @override_settings(TIME_ZONE="America/Sao_Paulo") def test_handle_dst_starting(self): dst_starting_date = datetime.datetime(2017, 10, 15, 0, 0, 0, 0) handled = handle_timezone(dst_starting_date, True) self.assertEqual( handled, make_aware(dst_starting_date, get_default_timezone(), True), ) class TranslateValidationDataTests(TestCase): class F(FilterSet): class Meta: model = Article fields = ["id", "author", "name"] choice = MultipleChoiceFilter(choices=[("1", "one"), ("2", "two")]) def test_error_detail(self): f = self.F( data={ "id": "foo", "author": "bar", "name": "baz", "choice": ["3"], } ) exc = translate_validation(f.errors) self.assertDictEqual( exc.detail, { "id": ["Enter a number."], "author": [ "Select a valid choice. That choice is not one of the available choices." ], "choice": [ "Select a valid choice. 3 is not one of the available choices." ], }, ) def test_full_error_details(self): f = self.F( data={ "id": "foo", "author": "bar", "name": "baz", "choice": ["3"], } ) exc = translate_validation(f.errors) self.assertEqual( exc.get_full_details(), { "id": [{"message": "Enter a number.", "code": "invalid"}], "author": [ { "message": "Select a valid choice. That choice is not one of the available choices.", "code": "invalid_choice", } ], "choice": [ { "message": "Select a valid choice. 3 is not one of the available choices.", "code": "invalid_choice", } ], }, ) django-filter-23.5/tests/test_forms.py0000644000175000017500000002041314533556010017753 0ustar carstencarstenfrom django import forms from django.test import TestCase, override_settings from django_filters.filters import CharFilter, ChoiceFilter from django_filters.filterset import FilterSet from .models import MANAGER, REGULAR, STATUS_CHOICES, Book, ManagerGroup, User class FilterSetFormTests(TestCase): def test_form_from_empty_filterset(self): class F(FilterSet): pass f = F(queryset=Book.objects.all()).form self.assertIsInstance(f, forms.Form) def test_form(self): class F(FilterSet): class Meta: model = Book fields = ("title",) f = F().form self.assertIsInstance(f, forms.Form) self.assertEqual(list(f.fields), ["title"]) def test_custom_form(self): class MyForm(forms.Form): pass class F(FilterSet): class Meta: model = Book fields = "__all__" form = MyForm f = F().form self.assertIsInstance(f, MyForm) def test_form_prefix(self): class F(FilterSet): class Meta: model = Book fields = ("title",) f = F().form self.assertIsNone(f.prefix) f = F(prefix="prefix").form self.assertEqual(f.prefix, "prefix") def test_form_fields(self): class F(FilterSet): class Meta: model = User fields = ["status"] f = F().form self.assertEqual(len(f.fields), 1) self.assertIn("status", f.fields) self.assertSequenceEqual( list(f.fields["status"].choices), (("", "---------"),) + STATUS_CHOICES ) def test_form_fields_exclusion(self): class F(FilterSet): title = CharFilter(exclude=True) class Meta: model = Book fields = ("title",) f = F().form self.assertEqual(f.fields["title"].label, "Exclude title") def test_complex_form_fields(self): class F(FilterSet): username = CharFilter(label="Filter for users with username") exclude_username = CharFilter( field_name="username", lookup_expr="iexact", exclude=True ) class Meta: model = User fields = { "status": ["exact", "lt", "gt"], "favorite_books__title": ["iexact", "in"], "manager_of__users__username": ["exact"], } fields = F().form.fields self.assertEqual(fields["username"].label, "Filter for users with username") self.assertEqual(fields["exclude_username"].label, "Exclude username") self.assertEqual(fields["status"].label, "Status") self.assertEqual(fields["status__lt"].label, "Status is less than") self.assertEqual(fields["status__gt"].label, "Status is greater than") self.assertEqual( fields["favorite_books__title__iexact"].label, "Favorite books title" ) self.assertEqual( fields["favorite_books__title__in"].label, "Favorite books title is in" ) self.assertEqual( fields["manager_of__users__username"].label, "Manager of users username" ) def test_form_fields_using_widget(self): class F(FilterSet): status = ChoiceFilter( widget=forms.RadioSelect, choices=STATUS_CHOICES, empty_label=None ) class Meta: model = User fields = ["status", "username"] f = F().form self.assertEqual(len(f.fields), 2) self.assertIn("status", f.fields) self.assertIn("username", f.fields) self.assertSequenceEqual(list(f.fields["status"].choices), STATUS_CHOICES) self.assertIsInstance(f.fields["status"].widget, forms.RadioSelect) def test_form_field_with_custom_label(self): class F(FilterSet): title = CharFilter(label="Book title") class Meta: model = Book fields = ("title",) f = F().form self.assertEqual(f.fields["title"].label, "Book title") self.assertEqual(f["title"].label, "Book title") def test_form_field_with_manual_name(self): class F(FilterSet): book_title = CharFilter(field_name="title") class Meta: model = Book fields = ("book_title",) f = F().form self.assertEqual(f.fields["book_title"].label, "Title") self.assertEqual(f["book_title"].label, "Title") def test_form_field_with_manual_name_and_label(self): class F(FilterSet): f1 = CharFilter(field_name="title", label="Book title") class Meta: model = Book fields = ("f1",) f = F().form self.assertEqual(f.fields["f1"].label, "Book title") self.assertEqual(f["f1"].label, "Book title") def test_filter_with_initial(self): class F(FilterSet): status = ChoiceFilter(choices=STATUS_CHOICES, initial=1) class Meta: model = User fields = ["status"] f = F().form self.assertEqual(f.fields["status"].initial, 1) def test_form_is_not_bound(self): class F(FilterSet): class Meta: model = Book fields = ("title",) f = F().form self.assertFalse(f.is_bound) self.assertEqual(f.data, {}) def test_form_is_bound(self): class F(FilterSet): class Meta: model = Book fields = ("title",) f = F({"title": "Some book"}).form self.assertTrue(f.is_bound) self.assertEqual(f.data, {"title": "Some book"}) def test_limit_choices_to(self): User.objects.create(username="inactive", is_active=False, status=REGULAR) User.objects.create(username="active", is_active=True, status=REGULAR) User.objects.create(username="manager", is_active=False, status=MANAGER) class F(FilterSet): class Meta: model = ManagerGroup fields = ["users", "manager"] f = F().form self.assertEqual(list(f.fields["users"].choices), [(2, "active")]) self.assertEqual( list(f.fields["manager"].choices), [("", "---------"), (3, "manager")] ) def test_disabled_help_text(self): class F(FilterSet): class Meta: model = Book fields = { # 'in' lookups are CSV-based, which have a `help_text`. "title": ["in"] } self.assertEqual( F().form.fields["title__in"].help_text, "Multiple values may be separated by commas.", ) with override_settings(FILTERS_DISABLE_HELP_TEXT=True): self.assertEqual(F().form.fields["title__in"].help_text, "") class FilterSetValidityTests(TestCase): class F(FilterSet): class Meta: model = Book fields = ["title", "price"] def test_not_bound(self): f = self.F() self.assertFalse(f.is_bound) self.assertFalse(f.is_valid()) self.assertEqual(f.data, {}) self.assertEqual(f.errors, {}) def test_is_bound_and_valid(self): f = self.F({"title": "Some book"}) self.assertTrue(f.is_bound) self.assertTrue(f.is_valid()) self.assertEqual(f.data, {"title": "Some book"}) self.assertEqual(f.errors, {}) def test_is_bound_and_not_valid(self): f = self.F({"price": "four dollars"}) self.assertTrue(f.is_bound) self.assertFalse(f.is_valid()) self.assertEqual(f.data, {"price": "four dollars"}) self.assertEqual(f.errors, {"price": ["Enter a number."]}) def test_number_filter_max_value_validation(self): class F(FilterSet): class Meta: model = Book fields = ["average_rating"] f = F({"average_rating": "1E1001"}) self.assertTrue(f.is_bound) self.assertFalse(f.is_valid()) self.assertEqual( f.errors, {"average_rating": ["Ensure this value is less than or equal to 1e+50."]}, ) django-filter-23.5/tests/rest_framework/0000755000175000017500000000000014533556010020246 5ustar carstencarstendjango-filter-23.5/tests/rest_framework/apps.py0000644000175000017500000000027014533556010021562 0ustar carstencarstenfrom django.apps import AppConfig class RestFrameworkTestConfig(AppConfig): name = "tests.rest_framework" label = "drf_test_app" verbose_name = "Rest Framework Test App" django-filter-23.5/tests/rest_framework/test_filterset.py0000644000175000017500000000351314533556010023662 0ustar carstencarstenfrom unittest import skipIf from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from django_filters.compat import crispy_forms from django_filters.rest_framework import FilterSet, filters from django_filters.widgets import BooleanWidget from ..models import Article, User class ArticleFilter(FilterSet): class Meta: model = Article fields = ["author"] class FilterSetFilterForFieldTests(TestCase): def test_isodatetimefilter(self): field = Article._meta.get_field("published") result = FilterSet.filter_for_field(field, "published") self.assertIsInstance(result, filters.IsoDateTimeFilter) self.assertEqual(result.field_name, "published") def test_booleanfilter_widget(self): field = User._meta.get_field("is_active") result = FilterSet.filter_for_field(field, "is_active") self.assertIsInstance(result, filters.BooleanFilter) self.assertEqual(result.extra["widget"], BooleanWidget) def test_booleanfilter_widget_nullbooleanfield(self): field = User._meta.get_field("is_employed") result = FilterSet.filter_for_field(field, "is_employed") self.assertIsInstance(result, filters.BooleanFilter) self.assertEqual(result.extra["widget"], BooleanWidget) @skipIf(crispy_forms is None, "django_crispy_forms must be installed") @override_settings(INSTALLED_APPS=settings.INSTALLED_APPS + ("crispy_forms",)) class CrispyFormsCompatTests(TestCase): def test_crispy_helper(self): # ensure the helper is present on the form self.assertTrue(hasattr(ArticleFilter().form, "helper")) def test_form_initialization(self): # ensure that crispy compat does not prematurely initialize the form self.assertFalse(hasattr(ArticleFilter(), "_form")) django-filter-23.5/tests/rest_framework/test_backends.py0000644000175000017500000003517114533556010023440 0ustar carstencarstenimport warnings from unittest import mock, skipIf from django.db.models import BooleanField from django.test import TestCase from django.test.utils import ignore_warnings, override_settings from rest_framework import generics, serializers from rest_framework.test import APIRequestFactory from django_filters import RemovedInDjangoFilter25Warning, compat, filters from django_filters.rest_framework import DjangoFilterBackend, FilterSet, backends from ..models import Article from .models import CategoryItem, FilterableItem factory = APIRequestFactory() class FilterableItemSerializer(serializers.ModelSerializer): class Meta: model = FilterableItem fields = "__all__" class CategoryItemSerializer(serializers.ModelSerializer): class Meta: model = CategoryItem fields = "__all__" # These class are used to test a filter class. class SeveralFieldsFilter(FilterSet): text = filters.CharFilter(lookup_expr="icontains") decimal = filters.NumberFilter(lookup_expr="lt") date = filters.DateFilter(lookup_expr="gt") class Meta: model = FilterableItem fields = ["text", "decimal", "date"] # Basic filter on a list view. class FilterableItemView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filter_backends = (DjangoFilterBackend,) class FilterFieldsRootView(FilterableItemView): filterset_fields = ["decimal", "date"] class FilterClassRootView(FilterableItemView): filterset_class = SeveralFieldsFilter class CategoryItemView(generics.ListCreateAPIView): queryset = CategoryItem.objects.all() serializer_class = CategoryItemSerializer filter_backends = (DjangoFilterBackend,) filterset_fields = ["category"] class GetFilterClassTests(TestCase): def test_filterset_class(self): class Filter(FilterSet): class Meta: model = FilterableItem fields = "__all__" backend = DjangoFilterBackend() view = FilterableItemView() view.filterset_class = Filter queryset = FilterableItem.objects.all() filterset_class = backend.get_filterset_class(view, queryset) self.assertIs(filterset_class, Filter) def test_filterset_class_no_meta(self): class Filter(FilterSet): pass backend = DjangoFilterBackend() view = FilterableItemView() view.filterset_class = Filter queryset = FilterableItem.objects.all() filterset_class = backend.get_filterset_class(view, queryset) self.assertIs(filterset_class, Filter) def test_filterset_class_no_queryset(self): class Filter(FilterSet): class Meta: model = FilterableItem fields = "__all__" backend = DjangoFilterBackend() view = FilterableItemView() view.filterset_class = Filter filterset_class = backend.get_filterset_class(view, None) self.assertIs(filterset_class, Filter) def test_filterset_fields(self): backend = DjangoFilterBackend() view = FilterableItemView() view.filterset_fields = ["text", "decimal", "date"] queryset = FilterableItem.objects.all() filterset_class = backend.get_filterset_class(view, queryset) self.assertEqual(filterset_class._meta.fields, view.filterset_fields) def test_filterset_fields_malformed(self): backend = DjangoFilterBackend() view = FilterableItemView() view.filterset_fields = ["non_existent"] queryset = FilterableItem.objects.all() msg = "'Meta.fields' must not contain non-model field names: non_existent" with self.assertRaisesMessage(TypeError, msg): backend.get_filterset_class(view, queryset) def test_filterset_fields_no_queryset(self): backend = DjangoFilterBackend() view = FilterableItemView() view.filterset_fields = ["text", "decimal", "date"] filterset_class = backend.get_filterset_class(view, None) self.assertIsNone(filterset_class) @skipIf(compat.coreapi is None, "coreapi must be installed") class GetSchemaFieldsTests(TestCase): def test_fields_with_filterset_fields_list(self): backend = DjangoFilterBackend() fields = backend.get_schema_fields(FilterFieldsRootView()) fields = [f.name for f in fields] self.assertEqual(fields, ["decimal", "date"]) def test_filterset_fields_list_with_bad_get_queryset(self): """ See: * https://github.com/carltongibson/django-filter/issues/551 """ class BadGetQuerySetView(FilterFieldsRootView): filterset_fields = ["decimal", "date"] def get_queryset(self): raise AttributeError("I don't have that") backend = DjangoFilterBackend() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") fields = backend.get_schema_fields(BadGetQuerySetView()) self.assertEqual( fields, [], "get_schema_fields should handle AttributeError" ) warning = "{} is not compatible with schema generation".format( BadGetQuerySetView ) self.assertEqual(len(w), 1) self.assertEqual(str(w[0].message), warning) def test_malformed_filterset_fields(self): # Malformed filter fields should raise an exception class View(FilterFieldsRootView): filterset_fields = ["non_existent"] backend = DjangoFilterBackend() msg = "'Meta.fields' must not contain non-model field names: non_existent" with self.assertRaisesMessage(TypeError, msg): backend.get_schema_fields(View()) def test_fields_with_filterset_fields_dict(self): class DictFilterFieldsRootView(FilterFieldsRootView): filterset_fields = { "decimal": ["exact", "lt", "gt"], } backend = DjangoFilterBackend() fields = backend.get_schema_fields(DictFilterFieldsRootView()) fields = [f.name for f in fields] self.assertEqual(fields, ["decimal", "decimal__lt", "decimal__gt"]) def test_fields_with_filterset_class(self): backend = DjangoFilterBackend() fields = backend.get_schema_fields(FilterClassRootView()) schemas = [f.schema for f in fields] fields = [f.name for f in fields] self.assertEqual(fields, ["text", "decimal", "date"]) self.assertIsInstance(schemas[0], compat.coreschema.String) self.assertIsInstance(schemas[1], compat.coreschema.Number) self.assertIsInstance(schemas[2], compat.coreschema.String) def test_field_required(self): class RequiredFieldsFilter(SeveralFieldsFilter): required_text = filters.CharFilter(required=True) class Meta(SeveralFieldsFilter.Meta): fields = SeveralFieldsFilter.Meta.fields + ["required_text"] class FilterClassWithRequiredFieldsView(FilterClassRootView): filterset_class = RequiredFieldsFilter backend = DjangoFilterBackend() fields = backend.get_schema_fields(FilterClassWithRequiredFieldsView()) required = [f.required for f in fields] fields = [f.name for f in fields] self.assertEqual(fields, ["text", "decimal", "date", "required_text"]) self.assertFalse(required[0]) self.assertFalse(required[1]) self.assertFalse(required[2]) self.assertTrue(required[3]) def tests_field_with_request_callable(self): def qs(request): # users expect a valid request object to be provided which cannot # be guaranteed during schema generation. self.fail( "callable queryset should not be invoked during schema generation" ) class F(SeveralFieldsFilter): f = filters.ModelChoiceFilter(queryset=qs) class View(FilterClassRootView): filterset_class = F view = View() view.request = factory.get("/") backend = DjangoFilterBackend() fields = backend.get_schema_fields(view) fields = [f.name for f in fields] self.assertEqual(fields, ["text", "decimal", "date", "f"]) class GetSchemaOperationParametersTests(TestCase): @ignore_warnings(category=RemovedInDjangoFilter25Warning) def test_get_operation_parameters_with_filterset_fields_list(self): backend = DjangoFilterBackend() fields = backend.get_schema_operation_parameters(FilterFieldsRootView()) fields = [f["name"] for f in fields] self.assertEqual(fields, ["decimal", "date"]) @ignore_warnings(category=RemovedInDjangoFilter25Warning) def test_get_operation_parameters_with_filterset_fields_list_with_choices(self): backend = DjangoFilterBackend() fields = backend.get_schema_operation_parameters(CategoryItemView()) self.assertEqual( fields, [ { "name": "category", "required": False, "in": "query", "description": "category", "schema": {"type": "string", "enum": ["home", "office"]}, } ], ) def test_deprecation_warning(self): backend = DjangoFilterBackend() msg = "Built-in schema generation is deprecated. Use drf-spectacular." with self.assertWarnsMessage(RemovedInDjangoFilter25Warning, msg): backend.get_schema_operation_parameters(FilterFieldsRootView()) class TemplateTests(TestCase): def test_backend_output(self): """ Ensure backend renders default if template path does not exist """ view = FilterFieldsRootView() backend = view.filter_backends[0] request = view.initialize_request(factory.get("/")) html = backend().to_html(request, view.get_queryset(), view) self.assertHTMLEqual( html, """

    Field filters

    """, ) def test_template_path(self): view = FilterFieldsRootView() class Backend(view.filter_backends[0]): template = "filter_template.html" request = view.initialize_request(factory.get("/")) html = Backend().to_html(request, view.get_queryset(), view) self.assertHTMLEqual(html, "Test") @override_settings(TEMPLATES=[]) def test_DTL_missing(self): # The backend should be importable even if the DTL is not used. # See: https://github.com/carltongibson/django-filter/issues/506 try: from importlib import reload # python 3.4 except ImportError: from imp import reload reload(backends) def test_multiple_engines(self): # See: https://github.com/carltongibson/django-filter/issues/578 DTL = { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, } ALT = { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "NAME": "alt", } # multiple DTL backends with override_settings(TEMPLATES=[DTL, ALT]): self.test_backend_output() class AutoFilterSetTests(TestCase): def test_autofilter_meta_inheritance(self): # https://github.com/carltongibson/django-filter/issues/663 class F(FilterSet): class Meta: filter_overrides = {BooleanField: {}} class Backend(DjangoFilterBackend): filterset_base = F view = FilterFieldsRootView() backend = Backend() filterset_class = backend.get_filterset_class(view, view.get_queryset()) filter_overrides = filterset_class._meta.filter_overrides # derived filterset_class.Meta should inherit from default_filter_set.Meta self.assertIn(BooleanField, filter_overrides) self.assertDictEqual(filter_overrides[BooleanField], {}) class ValidationErrorTests(TestCase): def test_errors(self): class F(FilterSet): class Meta: model = Article fields = ["id", "author", "name"] view = FilterFieldsRootView() backend = DjangoFilterBackend() request = factory.get("/?id=foo&author=bar&name=baz") request = view.initialize_request(request) queryset = Article.objects.all() view.filterset_class = F with self.assertRaises(serializers.ValidationError) as exc: backend.filter_queryset(request, queryset, view) # test output, does not include error code self.assertDictEqual( exc.exception.detail, { "id": ["Enter a number."], "author": [ "Select a valid choice. That choice is not one of the available choices." ], }, ) class DjangoFilterBackendTestCase(TestCase): @classmethod def setUpTestData(cls): cls.backend = DjangoFilterBackend() cls.backend.get_filterset_class = lambda x, y: None def test_get_filterset_none_filter_class(self): filterset = self.backend.get_filterset(mock.Mock(), mock.Mock(), mock.Mock()) self.assertIsNone(filterset) def test_filter_queryset_none_filter_class(self): prev_qs = mock.Mock() qs = self.backend.filter_queryset(mock.Mock(), prev_qs, mock.Mock()) self.assertIs(qs, prev_qs) def test_to_html_none_filter_class(self): html = self.backend.to_html(mock.Mock(), mock.Mock(), mock.Mock()) self.assertIsNone(html) @ignore_warnings(category=RemovedInDjangoFilter25Warning) def test_get_schema_operation_parameters_userwarning(self): with self.assertWarns(UserWarning): view = mock.Mock() view.__class__.return_value = "Test" view.get_queryset.side_effect = Exception self.backend.get_schema_operation_parameters(view) @mock.patch("django_filters.compat.is_crispy", return_value=True) def test_template_crispy(self, _): self.assertEqual( self.backend.template, "django_filters/rest_framework/crispy_form.html" ) django-filter-23.5/tests/rest_framework/test_integration.py0000644000175000017500000003635114533556010024212 0ustar carstencarstenimport datetime from decimal import Decimal from django.test import TestCase from django.test.utils import override_settings from django.urls import path, reverse from django.utils.dateparse import parse_date from rest_framework import generics, serializers, status from rest_framework.test import APIRequestFactory from django_filters import filters from django_filters.rest_framework import DjangoFilterBackend, FilterSet from .models import ( BaseFilterableItem, BasicModel, DjangoFilterOrderingModel, FilterableItem, ) factory = APIRequestFactory() class FilterableItemSerializer(serializers.ModelSerializer): class Meta: model = FilterableItem fields = "__all__" # Basic filter on a list view. class FilterFieldsRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filterset_fields = ["decimal", "date"] filter_backends = (DjangoFilterBackend,) # These class are used to test a filter class. class SeveralFieldsFilter(FilterSet): text = filters.CharFilter(lookup_expr="icontains") decimal = filters.NumberFilter(lookup_expr="lt") date = filters.DateFilter(lookup_expr="gt") class Meta: model = FilterableItem fields = ["text", "decimal", "date"] class FilterClassRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filterset_class = SeveralFieldsFilter filter_backends = (DjangoFilterBackend,) # These classes are used to test a misconfigured filter class. class MisconfiguredFilter(FilterSet): text = filters.CharFilter(lookup_expr="icontains") class Meta: model = BasicModel fields = ["text"] class IncorrectlyConfiguredRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filterset_class = MisconfiguredFilter filter_backends = (DjangoFilterBackend,) class FilterClassDetailView(generics.RetrieveAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filterset_class = SeveralFieldsFilter filter_backends = (DjangoFilterBackend,) # These classes are used to test base model filter support class BaseFilterableItemFilter(FilterSet): text = filters.CharFilter() class Meta: model = BaseFilterableItem fields = "__all__" class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filterset_class = BaseFilterableItemFilter filter_backends = (DjangoFilterBackend,) # Regression test for #814 class FilterFieldsQuerysetView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filterset_fields = ["decimal", "date"] filter_backends = (DjangoFilterBackend,) class GetQuerysetView(generics.ListCreateAPIView): serializer_class = FilterableItemSerializer filterset_class = SeveralFieldsFilter filter_backends = (DjangoFilterBackend,) def get_queryset(self): return FilterableItem.objects.all() urlpatterns = [ path("/", FilterClassDetailView.as_view(), name="detail-view"), path("", FilterClassRootView.as_view(), name="root-view"), path("get-queryset/", GetQuerysetView.as_view(), name="get-queryset-view"), ] class CommonFilteringTestCase(TestCase): def _serialize_object(self, obj): return { "id": obj.id, "text": obj.text, "decimal": str(obj.decimal), "date": obj.date.isoformat(), } def setUp(self): """ Create 10 FilterableItem instances. """ base_data = ("a", Decimal("0.25"), datetime.date(2012, 10, 8)) for i in range(10): text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. decimal = base_data[1] + i date = base_data[2] - datetime.timedelta(days=i * 2) FilterableItem(text=text, decimal=decimal, date=date).save() self.objects = FilterableItem.objects self.data = [self._serialize_object(obj) for obj in self.objects.all()] class IntegrationTestFiltering(CommonFilteringTestCase): """ Integration tests for filtered list views. """ def test_get_filtered_fields_root_view(self): """ GET requests to paginated ListCreateAPIView should return paginated results. """ view = FilterFieldsRootView.as_view() # Basic test with no filter. request = factory.get("/") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, self.data) # Tests that the decimal filter works. search_decimal = Decimal("2.25") request = factory.get("/", {"decimal": "%s" % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [ f for f in self.data if Decimal(f["decimal"]) == search_decimal ] self.assertEqual(response.data, expected_data) # Tests that the date filter works. search_date = datetime.date(2012, 9, 22) request = factory.get( "/", {"date": "%s" % search_date} ) # search_date str: '2012-09-22' response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if parse_date(f["date"]) == search_date] self.assertEqual(response.data, expected_data) def test_filter_with_queryset(self): """ Regression test for #814. """ view = FilterFieldsQuerysetView.as_view() # Tests that the decimal filter works. search_decimal = Decimal("2.25") request = factory.get("/", {"decimal": "%s" % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [ f for f in self.data if Decimal(f["decimal"]) == search_decimal ] self.assertEqual(response.data, expected_data) def test_filter_with_get_queryset_only(self): """ Regression test for #834. """ view = GetQuerysetView.as_view() request = factory.get("/get-queryset/") view(request).render() # Used to raise "issubclass() arg 2 must be a class or tuple of classes" # here when neither `model' nor `queryset' was specified. def test_get_filtered_class_root_view(self): """ GET requests to filtered ListCreateAPIView that have a filterset_class set should return filtered results. """ view = FilterClassRootView.as_view() # Basic test with no filter. request = factory.get("/") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, self.data) # Tests that the decimal filter set with 'lt' in the filter class works. search_decimal = Decimal("4.25") request = factory.get("/", {"decimal": "%s" % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if Decimal(f["decimal"]) < search_decimal] self.assertEqual(response.data, expected_data) # Tests that the date filter set with 'gt' in the filter class works. search_date = datetime.date(2012, 10, 2) request = factory.get( "/", {"date": "%s" % search_date} ) # search_date str: '2012-10-02' response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if parse_date(f["date"]) > search_date] self.assertEqual(response.data, expected_data) # Tests that the text filter set with 'icontains' in the filter class works. search_text = "ff" request = factory.get("/", {"text": "%s" % search_text}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if search_text in f["text"].lower()] self.assertEqual(response.data, expected_data) # Tests that multiple filters works. search_decimal = Decimal("5.25") search_date = datetime.date(2012, 10, 2) request = factory.get( "/", {"decimal": "%s" % (search_decimal,), "date": "%s" % (search_date,)} ) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [ f for f in self.data if parse_date(f["date"]) > search_date and Decimal(f["decimal"]) < search_decimal ] self.assertEqual(response.data, expected_data) def test_incorrectly_configured_filter(self): """ An error should be displayed when the filter class is misconfigured. """ view = IncorrectlyConfiguredRootView.as_view() request = factory.get("/") self.assertRaises(AssertionError, view, request) def test_base_model_filter(self): """ The `get_filterset_class` model checks should allow base model filters. """ view = BaseFilterableItemFilterRootView.as_view() request = factory.get("/?text=aaa") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) def test_unknown_filter(self): """ GET requests with filters that aren't configured should return 200. """ view = FilterFieldsRootView.as_view() search_integer = 10 request = factory.get("/", {"integer": "%s" % search_integer}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) def test_html_rendering(self): """ Make sure response renders w/ backend """ view = FilterFieldsRootView.as_view() request = factory.get("/") request.META["HTTP_ACCEPT"] = "text/html" response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) def test_raise_validation_error(self): """ Ensure validation errors return a proper error response instead of an internal server error. """ view = FilterFieldsRootView.as_view() request = factory.get("/?decimal=foobar") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data, {"decimal": ["Enter a number."]}) def test_permissive(self): """ Permissive handling should return a partially filtered result set. """ FilterableItem.objects.create(decimal=Decimal("1.23"), date="2017-01-01") FilterableItem.objects.create(decimal=Decimal("1.23"), date="2016-01-01") class Backend(DjangoFilterBackend): raise_exception = False class View(FilterFieldsRootView): filter_backends = (Backend,) view = View.as_view() request = factory.get("/?decimal=foobar&date=2017-01-01") response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data[0]["date"], "2017-01-01") self.assertEqual(len(response.data), 1) @override_settings(ROOT_URLCONF="tests.rest_framework.test_integration") class IntegrationTestDetailFiltering(CommonFilteringTestCase): """ Integration tests for filtered detail views. """ def _get_url(self, item): return reverse("detail-view", kwargs=dict(pk=item.pk)) def test_get_filtered_detail_view(self): """ GET requests to filtered RetrieveAPIView that have a filterset_class set should return filtered results. """ item = self.objects.all()[0] data = self._serialize_object(item) # Basic test with no filter. response = self.client.get(self._get_url(item)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, data) # Tests that the decimal filter set that should fail. search_decimal = Decimal("4.25") high_item = self.objects.filter(decimal__gt=search_decimal)[0] response = self.client.get( "{url}".format(url=self._get_url(high_item)), {"decimal": "{param}".format(param=search_decimal)}, ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # Tests that the decimal filter set that should succeed. search_decimal = Decimal("4.25") low_item = self.objects.filter(decimal__lt=search_decimal)[0] low_item_data = self._serialize_object(low_item) response = self.client.get( "{url}".format(url=self._get_url(low_item)), {"decimal": "{param}".format(param=search_decimal)}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, low_item_data) # Tests that multiple filters works. search_decimal = Decimal("5.25") search_date = datetime.date(2012, 10, 2) valid_item = self.objects.filter( decimal__lt=search_decimal, date__gt=search_date )[0] valid_item_data = self._serialize_object(valid_item) response = self.client.get( "{url}".format(url=self._get_url(valid_item)), { "decimal": "{decimal}".format(decimal=search_decimal), "date": "{date}".format(date=search_date), }, ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, valid_item_data) class DjangoFilterOrderingSerializer(serializers.ModelSerializer): class Meta: model = DjangoFilterOrderingModel fields = "__all__" class DjangoFilterOrderingTests(TestCase): def setUp(self): data = [ {"date": datetime.date(2012, 10, 8), "text": "abc"}, {"date": datetime.date(2013, 10, 8), "text": "bcd"}, {"date": datetime.date(2014, 10, 8), "text": "cde"}, ] for d in data: DjangoFilterOrderingModel.objects.create(**d) def test_default_ordering(self): class DjangoFilterOrderingView(generics.ListAPIView): serializer_class = DjangoFilterOrderingSerializer queryset = DjangoFilterOrderingModel.objects.all() filter_backends = (DjangoFilterBackend,) filterset_fields = ["text"] ordering = ("-date",) view = DjangoFilterOrderingView.as_view() request = factory.get("/") response = view(request) self.assertEqual( response.data, [ {"id": 3, "date": "2014-10-08", "text": "cde"}, {"id": 2, "date": "2013-10-08", "text": "bcd"}, {"id": 1, "date": "2012-10-08", "text": "abc"}, ], ) django-filter-23.5/tests/rest_framework/__init__.py0000644000175000017500000000011114533556010022350 0ustar carstencarstendefault_app_config = "tests.rest_framework.apps.RestFrameworkTestConfig" django-filter-23.5/tests/rest_framework/templates/0000755000175000017500000000000014533556010022244 5ustar carstencarstendjango-filter-23.5/tests/rest_framework/templates/filter_template.html0000644000175000017500000000000514533556010026305 0ustar carstencarstenTest django-filter-23.5/tests/rest_framework/models.py0000644000175000017500000000145114533556010022104 0ustar carstencarstenfrom django.db import models from django.utils.translation import gettext_lazy as _ class BasicModel(models.Model): text = models.CharField( max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."), ) class BaseFilterableItem(models.Model): text = models.CharField(max_length=100) class FilterableItem(BaseFilterableItem): decimal = models.DecimalField(max_digits=4, decimal_places=2) date = models.DateField() class DjangoFilterOrderingModel(models.Model): date = models.DateField() text = models.CharField(max_length=10) class Meta: ordering = ["-date"] class CategoryItem(BaseFilterableItem): category = models.CharField( max_length=10, choices=(("home", "Home"), ("office", "Office")) ) django-filter-23.5/tests/rest_framework/test_filters.py0000644000175000017500000000207014533556010023326 0ustar carstencarstenimport inspect from django.test import TestCase from django_filters.rest_framework import filters from django_filters.widgets import BooleanWidget class ModuleImportTests(TestCase): def is_filter(self, name, value): return isinstance(value, type) and issubclass(value, filters.Filter) def test_imports(self): # msg = "Expected `filters.%s` to be imported in `filters.__all__`" filter_classes = [ key for key, value in inspect.getmembers(filters) if isinstance(value, type) and issubclass(value, filters.Filter) ] # sanity check self.assertIn("Filter", filter_classes) self.assertIn("BooleanFilter", filter_classes) for f in filter_classes: self.assertIn(f, filters.__all__) class BooleanFilterTests(TestCase): def test_widget(self): # Ensure that `BooleanFilter` uses the correct widget when importing # from `rest_framework.filters`. f = filters.BooleanFilter() self.assertEqual(f.extra["widget"], BooleanWidget) django-filter-23.5/tests/test_filtering.py0000644000175000017500000022055214533556010020616 0ustar carstencarstenimport contextlib import datetime import unittest from operator import attrgetter from unittest import mock import django from django import forms from django.http import QueryDict from django.test import override_settings from django.utils import timezone from django.utils.timezone import make_aware, now from django_filters.compat import TestCase from django_filters.filters import ( AllValuesFilter, AllValuesMultipleFilter, BaseInFilter, CharFilter, ChoiceFilter, DateFromToRangeFilter, DateRangeFilter, DateTimeFromToRangeFilter, DurationFilter, IsoDateTimeFromToRangeFilter, LookupChoiceFilter, ModelChoiceFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter, OrderingFilter, RangeFilter, TimeRangeFilter, TypedMultipleChoiceFilter, ) from django_filters.filterset import FilterSet from django_filters.widgets import QueryArrayWidget from .models import ( STATUS_CHOICES, Account, Article, BankAccount, Book, Comment, Company, DirectedNode, Location, Node, Profile, SpacewalkRecord, User, ) from .utils import MockQuerySet class CharFilterTests(TestCase): def test_filtering(self): b1 = Book.objects.create(title="Ender's Game", price="1.00", average_rating=3.0) b2 = Book.objects.create(title="Rainbow Six", price="1.00", average_rating=3.0) b3 = Book.objects.create(title="Snowcrash", price="1.00", average_rating=3.0) class F(FilterSet): class Meta: model = Book fields = ["title"] qs = Book.objects.all() f = F(queryset=qs) self.assertQuerySetEqual( f.qs, [b1.pk, b2.pk, b3.pk], lambda o: o.pk, ordered=False ) f = F({"title": "Snowcrash"}, queryset=qs) self.assertQuerySetEqual(f.qs, [b3.pk], lambda o: o.pk) class IntegerFilterTest(TestCase): def test_filtering(self): default_values = { "in_good_standing": True, "friendly": False, } b1 = BankAccount.objects.create(amount_saved=0, **default_values) b2 = BankAccount.objects.create(amount_saved=3, **default_values) b3 = BankAccount.objects.create(amount_saved=10, **default_values) class F(FilterSet): class Meta: model = BankAccount fields = ["amount_saved"] qs = BankAccount.objects.all() f = F(queryset=qs) self.assertQuerySetEqual( f.qs, [b1.pk, b2.pk, b3.pk], lambda o: o.pk, ordered=False ) f = F({"amount_saved": "10"}, queryset=qs) self.assertQuerySetEqual(f.qs, [b3.pk], lambda o: o.pk) f = F({"amount_saved": "0"}, queryset=qs) self.assertQuerySetEqual(f.qs, [b1.pk], lambda o: o.pk) class BooleanFilterTests(TestCase): def test_filtering(self): User.objects.create(username="alex", is_active=False) User.objects.create(username="jacob", is_active=True) User.objects.create(username="aaron", is_active=False) class F(FilterSet): class Meta: model = User fields = ["is_active"] qs = User.objects.all() # '2' and '3' are how the field expects the data from the browser f = F({"is_active": "2"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["jacob"], lambda o: o.username, False) f = F({"is_active": "3"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex", "aaron"], lambda o: o.username, False) f = F({"is_active": "1"}, queryset=qs) self.assertQuerySetEqual( f.qs, ["alex", "aaron", "jacob"], lambda o: o.username, False ) class ChoiceFilterTests(TestCase): @classmethod def setUpTestData(cls): User.objects.create(username="alex", status=1) User.objects.create(username="jacob", status=2) User.objects.create(username="aaron", status=2) User.objects.create(username="carl", status=0) Article.objects.create(author_id=1, published=now()) Article.objects.create(author_id=2, published=now()) Article.objects.create(author_id=3, published=now()) Article.objects.create(author_id=4, published=now()) Article.objects.create(author_id=None, published=now()) def test_filtering(self): class F(FilterSet): class Meta: model = User fields = ["status"] f = F() self.assertQuerySetEqual( f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False ) f = F({"status": "1"}) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username, False) f = F({"status": "2"}) self.assertQuerySetEqual(f.qs, ["jacob", "aaron"], lambda o: o.username, False) f = F({"status": "0"}) self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username, False) def test_filtering_on_explicitly_defined_field(self): """ Test for #30. If you explicitly declare ChoiceFilter fields you **MUST** pass `choices`. """ class F(FilterSet): status = ChoiceFilter(choices=STATUS_CHOICES) class Meta: model = User fields = ["status"] f = F() self.assertQuerySetEqual( f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False ) f = F({"status": "1"}) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username, False) f = F({"status": "2"}) self.assertQuerySetEqual(f.qs, ["jacob", "aaron"], lambda o: o.username, False) f = F({"status": "0"}) self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username, False) def test_filtering_on_empty_choice(self): class F(FilterSet): class Meta: model = User fields = ["status"] f = F({"status": ""}) self.assertQuerySetEqual( f.qs, ["aaron", "alex", "jacob", "carl"], lambda o: o.username, False ) def test_filtering_on_null_choice(self): choices = [(u.pk, str(u)) for u in User.objects.order_by("id")] class F(FilterSet): author = ChoiceFilter( choices=choices, null_value="null", null_label="NULL", ) class Meta: model = Article fields = ["author"] # sanity check to make sure the filter is setup correctly f = F({"author": "1"}) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: str(o.author), False) f = F({"author": "null"}) self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) class MultipleChoiceFilterTests(TestCase): def test_filtering(self): User.objects.create(username="alex", status=1) User.objects.create(username="jacob", status=2) User.objects.create(username="aaron", status=2) User.objects.create(username="carl", status=0) class F(FilterSet): status = MultipleChoiceFilter(choices=STATUS_CHOICES) class Meta: model = User fields = ["status"] qs = User.objects.all().order_by("username") f = F(queryset=qs) self.assertQuerySetEqual( f.qs, ["aaron", "jacob", "alex", "carl"], lambda o: o.username, False ) f = F({"status": ["0"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username) f = F({"status": ["0", "1"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex", "carl"], lambda o: o.username) f = F({"status": ["0", "1", "2"]}, queryset=qs) self.assertQuerySetEqual( f.qs, ["aaron", "alex", "carl", "jacob"], lambda o: o.username ) def test_filtering_on_null_choice(self): User.objects.create(username="alex", status=1) User.objects.create(username="jacob", status=2) User.objects.create(username="aaron", status=2) User.objects.create(username="carl", status=0) Article.objects.create(author_id=1, published=now()) Article.objects.create(author_id=2, published=now()) Article.objects.create(author_id=3, published=now()) Article.objects.create(author_id=4, published=now()) Article.objects.create(author_id=None, published=now()) choices = [(u.pk, str(u)) for u in User.objects.order_by("id")] class F(FilterSet): author = MultipleChoiceFilter( choices=choices, null_value="null", null_label="NULL", ) class Meta: model = Article fields = ["author"] # sanity check to make sure the filter is setup correctly f = F({"author": ["1"]}) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: str(o.author), False) f = F({"author": ["null"]}) self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) f = F({"author": ["1", "null"]}) self.assertQuerySetEqual( f.qs, ["alex", None], lambda o: o.author and str(o.author), False ) class TypedMultipleChoiceFilterTests(TestCase): def test_filtering(self): User.objects.create(username="alex", status=1) User.objects.create(username="jacob", status=2) User.objects.create(username="aaron", status=2) User.objects.create(username="carl", status=0) class F(FilterSet): status = TypedMultipleChoiceFilter( choices=STATUS_CHOICES, coerce=lambda x: x[0:2] ) class Meta: model = User fields = ["status"] qs = User.objects.all().order_by("username") f = F(queryset=qs) self.assertQuerySetEqual( f.qs, ["aa", "ja", "al", "ca"], lambda o: o.username[0:2], False ) f = F({"status": ["0"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["ca"], lambda o: o.username[0:2]) f = F({"status": ["0", "1"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["al", "ca"], lambda o: o.username[0:2]) f = F({"status": ["0", "1", "2"]}, queryset=qs) self.assertQuerySetEqual( f.qs, ["aa", "al", "ca", "ja"], lambda o: o.username[0:2] ) class DateFilterTests(TestCase): def test_filtering(self): today = now().date() timestamp = now().time().replace(microsecond=0) last_week = today - datetime.timedelta(days=7) check_date = str(last_week) u = User.objects.create(username="alex") Comment.objects.create(author=u, time=timestamp, date=today) Comment.objects.create(author=u, time=timestamp, date=last_week) Comment.objects.create(author=u, time=timestamp, date=today) Comment.objects.create(author=u, time=timestamp, date=last_week) class F(FilterSet): class Meta: model = Comment fields = ["date"] f = F({"date": check_date}, queryset=Comment.objects.all()) self.assertEqual(len(f.qs), 2) self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk, False) class TimeFilterTests(TestCase): def test_filtering(self): today = now().date() now_time = now().time().replace(microsecond=0) ten_min_ago = now() - datetime.timedelta(minutes=10) fixed_time = ten_min_ago.time().replace(microsecond=0) check_time = str(fixed_time) u = User.objects.create(username="alex") Comment.objects.create(author=u, time=now_time, date=today) Comment.objects.create(author=u, time=fixed_time, date=today) Comment.objects.create(author=u, time=now_time, date=today) Comment.objects.create(author=u, time=fixed_time, date=today) class F(FilterSet): class Meta: model = Comment fields = ["time"] f = F({"time": check_time}, queryset=Comment.objects.all()) self.assertEqual(len(f.qs), 2) self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk, False) class DateTimeFilterTests(TestCase): def test_filtering(self): now_dt = now() ten_min_ago = now_dt - datetime.timedelta(minutes=10) one_day_ago = now_dt - datetime.timedelta(days=1) u = User.objects.create(username="alex") Article.objects.create(author=u, published=now_dt) Article.objects.create(author=u, published=ten_min_ago) Article.objects.create(author=u, published=one_day_ago) tz = timezone.get_current_timezone() # make naive, like a browser would send local_ten_min_ago = timezone.make_naive(ten_min_ago, tz) check_dt = str(local_ten_min_ago) class F(FilterSet): class Meta: model = Article fields = ["published"] qs = Article.objects.all() f = F({"published": ten_min_ago}, queryset=qs) self.assertEqual(len(f.qs), 1) self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) # this is how it would come through a browser f = F({"published": check_dt}, queryset=qs) self.assertEqual( len(f.qs), 1, "%s isn't matching %s when cleaned" % (check_dt, ten_min_ago) ) self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) class DurationFilterTests(TestCase): """Duration filter tests. The preferred format for durations in Django is '%d %H:%M:%S.%f'. See django.utils.dateparse.parse_duration Django is not fully ISO 8601 compliant (yet): year, month, and week designators are not supported, so a duration string like "P3Y6M4DT12H30M5S" cannot be used. See https://en.wikipedia.org/wiki/ISO_8601#Durations """ def setUp(self): self.r1 = SpacewalkRecord.objects.create( astronaut="Anatoly Solovyev", duration=datetime.timedelta(hours=82, minutes=22), ) self.r2 = SpacewalkRecord.objects.create( astronaut="Michael Lopez-Alegria", duration=datetime.timedelta(hours=67, minutes=40), ) self.r3 = SpacewalkRecord.objects.create( astronaut="Jerry L. Ross", duration=datetime.timedelta(hours=58, minutes=32) ) self.r4 = SpacewalkRecord.objects.create( astronaut="John M. Grunsfeld", duration=datetime.timedelta(hours=58, minutes=30), ) self.r5 = SpacewalkRecord.objects.create( astronaut="Richard Mastracchio", duration=datetime.timedelta(hours=53, minutes=4), ) def test_filtering(self): class F(FilterSet): class Meta: model = SpacewalkRecord fields = ["duration"] qs = SpacewalkRecord.objects.all() # Django style: 3 days, 10 hours, 22 minutes. f = F({"duration": "3 10:22:00"}, queryset=qs) self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # ISO 8601: 3 days, 10 hours, 22 minutes. f = F({"duration": "P3DT10H22M"}, queryset=qs) self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # Django style: 82 hours, 22 minutes. f = F({"duration": "82:22:00"}, queryset=qs) self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) # ISO 8601: 82 hours, 22 minutes. f = F({"duration": "PT82H22M"}, queryset=qs) self.assertQuerySetEqual(f.qs, [self.r1], lambda x: x) def test_filtering_with_single_lookup_expr_dictionary(self): class F(FilterSet): class Meta: model = SpacewalkRecord fields = {"duration": ["gt", "gte", "lt", "lte"]} qs = SpacewalkRecord.objects.order_by("-duration") f = F({"duration__gt": "PT58H30M"}, queryset=qs) self.assertQuerySetEqual(f.qs, [self.r1, self.r2, self.r3], lambda x: x) f = F({"duration__gte": "PT58H30M"}, queryset=qs) self.assertQuerySetEqual( f.qs, [self.r1, self.r2, self.r3, self.r4], lambda x: x ) f = F({"duration__lt": "PT58H30M"}, queryset=qs) self.assertQuerySetEqual(f.qs, [self.r5], lambda x: x) f = F({"duration__lte": "PT58H30M"}, queryset=qs) self.assertQuerySetEqual(f.qs, [self.r4, self.r5], lambda x: x) def test_filtering_with_multiple_lookup_exprs(self): class F(FilterSet): min_duration = DurationFilter(field_name="duration", lookup_expr="gte") max_duration = DurationFilter(field_name="duration", lookup_expr="lte") class Meta: model = SpacewalkRecord fields = "__all__" qs = SpacewalkRecord.objects.order_by("duration") f = F({"min_duration": "PT55H", "max_duration": "PT60H"}, queryset=qs) self.assertQuerySetEqual(f.qs, [self.r4, self.r3], lambda x: x) class ModelChoiceFilterTests(TestCase): def test_filtering(self): alex = User.objects.create(username="alex") jacob = User.objects.create(username="jacob") date = now().date() time = now().time() Comment.objects.create(author=jacob, time=time, date=date) Comment.objects.create(author=alex, time=time, date=date) Comment.objects.create(author=jacob, time=time, date=date) class F(FilterSet): class Meta: model = Comment fields = ["author"] qs = Comment.objects.all() f = F({"author": jacob.pk}, queryset=qs) self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) @override_settings(FILTERS_NULL_CHOICE_LABEL="No Author") def test_filtering_null(self): Article.objects.create(published=now()) alex = User.objects.create(username="alex") Article.objects.create(author=alex, published=now()) class F(FilterSet): class Meta: model = Article fields = ["author", "name"] qs = Article.objects.all() f = F({"author": "null"}, queryset=qs) self.assertQuerySetEqual(f.qs, [None], lambda o: o.author, False) def test_callable_queryset(self): # Sanity check for callable queryset arguments. # Ensure that nothing is improperly cached User.objects.create(username="alex") jacob = User.objects.create(username="jacob") aaron = User.objects.create(username="aaron") def users(request): return User.objects.filter(pk__lt=request.user.pk) class F(FilterSet): author = ModelChoiceFilter(field_name="author", queryset=users) class Meta: model = Comment fields = ["author"] qs = Comment.objects.all() request = mock.Mock() request.user = jacob f = F(queryset=qs, request=request).filters["author"].field self.assertQuerySetEqual(f.queryset, [1], lambda o: o.pk, False) request.user = aaron f = F(queryset=qs, request=request).filters["author"].field self.assertQuerySetEqual(f.queryset, [1, 2], lambda o: o.pk, False) class ModelMultipleChoiceFilterTests(TestCase): def setUp(self): alex = User.objects.create(username="alex") User.objects.create(username="jacob") aaron = User.objects.create(username="aaron") b1 = Book.objects.create(title="Ender's Game", price="1.00", average_rating=3.0) b2 = Book.objects.create(title="Rainbow Six", price="1.00", average_rating=3.0) b3 = Book.objects.create(title="Snowcrash", price="1.00", average_rating=3.0) Book.objects.create( title="Stranger in a Strage Land", price="1.00", average_rating=3.0 ) alex.favorite_books.add(b1, b2) aaron.favorite_books.add(b1, b3) self.alex = alex def test_filtering(self): class F(FilterSet): class Meta: model = User fields = ["favorite_books"] qs = User.objects.all().order_by("username") f = F({"favorite_books": ["1"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["1", "3"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["2"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) f = F({"favorite_books": ["4"]}, queryset=qs) self.assertQuerySetEqual(f.qs, [], lambda o: o.username) @override_settings(FILTERS_NULL_CHOICE_LABEL="No Favorites") def test_filtering_null(self): class F(FilterSet): class Meta: model = User fields = ["favorite_books"] qs = User.objects.all() f = F({"favorite_books": ["null"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["jacob"], lambda o: o.username) def test_filtering_dictionary(self): class F(FilterSet): class Meta: model = User fields = {"favorite_books": ["exact"]} qs = User.objects.all().order_by("username") f = F({"favorite_books": ["1"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["1", "3"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["2"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) f = F({"favorite_books": ["4"]}, queryset=qs) self.assertQuerySetEqual(f.qs, [], lambda o: o.username) def test_filtering_on_all_of_subset_of_choices(self): class F(FilterSet): class Meta: model = User fields = ["favorite_books"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # This filter has a limited number of choices. self.filters["favorite_books"].extra.update( {"queryset": Book.objects.filter(id__in=[1, 2])} ) self.filters["favorite_books"].extra["required"] = True qs = User.objects.all().order_by("username") # Select all the given choices. f = F({"favorite_books": ["1", "2"]}, queryset=qs) # The results should only include matching users - not Jacob. self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) def test_filtering_on_non_required_fields(self): # See issue #132 - filtering with all options on a non-required # field should exclude any results where the field is null. class F(FilterSet): author = ModelMultipleChoiceFilter(queryset=User.objects.all()) class Meta: model = Article fields = ["author"] published = now() Article.objects.create(published=published, author=self.alex) Article.objects.create(published=published, author=self.alex) Article.objects.create(published=published) qs = Article.objects.all() # Select all authors. authors = [str(user.id) for user in User.objects.all()] f = F({"author": authors}, queryset=qs) # The results should not include anonymous articles self.assertEqual( set(f.qs), set(Article.objects.exclude(author__isnull=True)), ) class NumberFilterTests(TestCase): def setUp(self): Book.objects.create( title="Ender's Game", price="10.0", average_rating=4.7999999999999998 ) Book.objects.create( title="Rainbow Six", price="15.0", average_rating=4.5999999999999996 ) Book.objects.create( title="Snowcrash", price="20.0", average_rating=4.2999999999999998 ) def test_filtering(self): class F(FilterSet): class Meta: model = Book fields = ["price"] f = F({"price": 10}, queryset=Book.objects.all()) self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) class RangeFilterTests(TestCase): def setUp(self): Book.objects.create( title="Ender's Game", price="10.0", average_rating=4.7999999999999998 ) Book.objects.create( title="Rainbow Six", price="15.0", average_rating=4.5999999999999996 ) Book.objects.create( title="Snowcrash", price="20.0", average_rating=4.2999999999999998 ) Book.objects.create(title="Refund", price="-10.0", average_rating=5.0) Book.objects.create(title="Free Book", price="0.0", average_rating=0.0) def test_filtering(self): class F(FilterSet): price = RangeFilter() class Meta: model = Book fields = ["price"] qs = Book.objects.all().order_by("title") f = F(queryset=qs) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Free Book", "Rainbow Six", "Refund", "Snowcrash"], lambda o: o.title, ) f = F({"price_min": "5", "price_max": "15"}, queryset=qs) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) f = F({"price_min": "11"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["Rainbow Six", "Snowcrash"], lambda o: o.title) f = F({"price_max": "19"}, queryset=qs) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Free Book", "Rainbow Six", "Refund"], lambda o: o.title, ) f = F({"price_min": "0", "price_max": "12"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["Ender's Game", "Free Book"], lambda o: o.title) f = F({"price_min": "-11", "price_max": "0"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["Free Book", "Refund"], lambda o: o.title) f = F({"price_min": "0", "price_max": "0"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["Free Book"], lambda o: o.title) class DateRangeFilterTests(TestCase): class CommentFilter(FilterSet): date = DateRangeFilter() class Meta: model = Comment fields = ["date"] @contextlib.contextmanager def relative_to(self, today): today = make_aware(today) yesterday = today - datetime.timedelta(days=1) five_days_ago = today - datetime.timedelta(days=5) two_weeks_ago = today - datetime.timedelta(days=14) two_months_ago = today - datetime.timedelta(days=62) two_years_ago = today - datetime.timedelta(days=800) alex = User.objects.create(username="alex") time = now().time() Comment.objects.create(date=two_weeks_ago, author=alex, time=time) Comment.objects.create(date=two_years_ago, author=alex, time=time) Comment.objects.create(date=five_days_ago, author=alex, time=time) Comment.objects.create(date=today, author=alex, time=time) Comment.objects.create(date=yesterday, author=alex, time=time) Comment.objects.create(date=two_months_ago, author=alex, time=time) with mock.patch("django_filters.filters.now") as mock_now: mock_now.return_value = today yield def test_filtering_for_year(self): f = self.CommentFilter({"date": "year"}) with self.relative_to(datetime.datetime(now().year, 4, 1)): self.assertQuerySetEqual(f.qs, [1, 3, 4, 5, 6], lambda o: o.pk, False) def test_filtering_for_month(self): f = self.CommentFilter({"date": "month"}) with self.relative_to(datetime.datetime(now().year, 4, 21)): self.assertQuerySetEqual(f.qs, [1, 3, 4, 5], lambda o: o.pk, False) def test_filtering_for_week(self): f = self.CommentFilter({"date": "week"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): self.assertQuerySetEqual(f.qs, [3, 4, 5], lambda o: o.pk, False) def test_filtering_for_yesterday(self): f = self.CommentFilter({"date": "yesterday"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): self.assertQuerySetEqual(f.qs, [5], lambda o: o.pk, False) def test_filtering_for_today(self): f = self.CommentFilter({"date": "today"}) with self.relative_to(datetime.datetime(now().year, 1, 1)): self.assertQuerySetEqual(f.qs, [4], lambda o: o.pk, False) class DateFromToRangeFilterTests(TestCase): def test_filtering(self): adam = User.objects.create(username="adam") kwargs = {"text": "test", "author": adam, "time": "10:00"} Comment.objects.create(date=datetime.date(2016, 1, 1), **kwargs) Comment.objects.create(date=datetime.date(2016, 1, 2), **kwargs) Comment.objects.create(date=datetime.date(2016, 1, 3), **kwargs) Comment.objects.create(date=datetime.date(2016, 1, 3), **kwargs) class F(FilterSet): published = DateFromToRangeFilter(field_name="date") class Meta: model = Comment fields = ["date"] results = F( data={"published_after": "2016-01-02", "published_before": "2016-01-03"} ) self.assertEqual(len(results.qs), 3) def test_filtering_ignores_time(self): tz = timezone.get_current_timezone() Article.objects.create( published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz) ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article fields = ["published"] results = F( data={"published_after": "2016-01-02", "published_before": "2016-01-03"} ) self.assertEqual(len(results.qs), 3) @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") @override_settings(TIME_ZONE="America/Sao_Paulo") def test_filtering_dst_start_midnight(self): tz = timezone.get_default_timezone() Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 14, 23, 59), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 15, 0, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 15, 1, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 16, 0, 0), tz, False) ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article fields = ["published"] results = F( data={"published_after": "2017-10-15", "published_before": "2017-10-15"} ) self.assertEqual(len(results.qs), 2) @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") @override_settings(TIME_ZONE="America/Sao_Paulo") def test_filtering_dst_ends_midnight(self): tz = timezone.get_default_timezone() Article.objects.create( published=make_aware(datetime.datetime(2017, 2, 19, 0, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 2, 18, 23, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 2, 18, 0, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 2, 17, 15, 0), tz, False) ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article fields = ["published"] results = F( data={"published_after": "2017-02-18", "published_before": "2017-02-18"} ) self.assertEqual(len(results.qs), 2) @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") @override_settings(TIME_ZONE="Europe/Paris") def test_filtering_dst_start(self): tz = timezone.get_default_timezone() Article.objects.create( published=make_aware(datetime.datetime(2017, 3, 25, 23, 59), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 3, 26, 0, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 3, 26, 2, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 3, 26, 3, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 3, 27, 0, 0), tz, False) ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article fields = ["published"] results = F( data={"published_after": "2017-3-26", "published_before": "2017-3-26"} ) self.assertEqual(len(results.qs), 3) @unittest.skipUnless(django.VERSION < (5, 0), "is_dst removed in Django 5.0") @override_settings(TIME_ZONE="Europe/Paris") def test_filtering_dst_end(self): tz = timezone.get_default_timezone() Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 28, 23, 59), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 29, 0, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 29, 2, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 29, 3, 0), tz, False) ) Article.objects.create( published=make_aware(datetime.datetime(2017, 10, 30, 0, 0), tz, False) ) class F(FilterSet): published = DateFromToRangeFilter() class Meta: model = Article fields = ["published"] results = F( data={"published_after": "2017-10-29", "published_before": "2017-10-29"} ) self.assertEqual(len(results.qs), 3) class DateTimeFromToRangeFilterTests(TestCase): def test_filtering(self): tz = timezone.get_current_timezone() Article.objects.create( published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz) ) class F(FilterSet): published = DateTimeFromToRangeFilter() class Meta: model = Article fields = ["published"] results = F( data={ "published_after": "2016-01-02 10:00", "published_before": "2016-01-03 19:00", } ) self.assertEqual(len(results.qs), 2) class IsoDateTimeFromToRangeFilterTests(TestCase): def test_filtering(self): tz = timezone.get_current_timezone() Article.objects.create( published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz) ) Article.objects.create( published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz) ) class F(FilterSet): published = IsoDateTimeFromToRangeFilter() class Meta: model = Article fields = ["published"] dt = datetime.datetime.now(tz=tz) results = F( data={ "published_after": "2016-01-02T10:00:00.000000" + dt.strftime("%z"), "published_before": "2016-01-03T19:00:00.000000" + dt.strftime("%z"), } ) self.assertEqual(len(results.qs), 2) class TimeRangeFilterTests(TestCase): def test_filtering(self): adam = User.objects.create(username="adam") kwargs = {"text": "test", "author": adam, "date": datetime.date.today()} Comment.objects.create(time="7:30", **kwargs) Comment.objects.create(time="8:00", **kwargs) Comment.objects.create(time="9:30", **kwargs) Comment.objects.create(time="11:00", **kwargs) class F(FilterSet): time = TimeRangeFilter() class Meta: model = Comment fields = ["time"] results = F(data={"time_after": "8:00", "time_before": "10:00"}) self.assertEqual(len(results.qs), 2) class AllValuesFilterTests(TestCase): def test_filtering(self): User.objects.create(username="alex") User.objects.create(username="jacob") User.objects.create(username="aaron") class F(FilterSet): username = AllValuesFilter() class Meta: model = User fields = ["username"] self.assertEqual(list(F().qs), list(User.objects.all())) self.assertEqual( list(F({"username": "alex"}).qs), [User.objects.get(username="alex")] ) # invalid choice self.assertFalse(F({"username": "jose"}).is_valid()) self.assertEqual(list(F({"username": "jose"}).qs), list(User.objects.all())) class AllValuesMultipleFilterTests(TestCase): def test_filtering(self): User.objects.create(username="alex") User.objects.create(username="jacob") User.objects.create(username="aaron") class F(FilterSet): username = AllValuesMultipleFilter() class Meta: model = User fields = ["username"] self.assertEqual(list(F().qs), list(User.objects.all())) self.assertEqual( list(F({"username": ["alex"]}).qs), [User.objects.get(username="alex")] ) self.assertEqual( list(F({"username": ["alex", "jacob"]}).qs), list(User.objects.filter(username__in=["alex", "jacob"])), ) # invalid choice self.assertFalse(F({"username": "jose"}).is_valid()) self.assertEqual(list(F({"username": "jose"}).qs), list(User.objects.all())) class FilterMethodTests(TestCase): def setUp(self): User.objects.create(username="alex") User.objects.create(username="jacob") User.objects.create(username="aaron") def test_filtering(self): class F(FilterSet): username = CharFilter(method="filter_username") class Meta: model = User fields = ["username"] def filter_username(self, queryset, name, value): return queryset.filter(**{name: value}) self.assertEqual(list(F().qs), list(User.objects.all())) self.assertEqual( list(F({"username": "alex"}).qs), [User.objects.get(username="alex")] ) self.assertEqual(list(F({"username": "jose"}).qs), list()) def test_filtering_callable(self): def filter_username(queryset, name, value): return queryset.filter(**{name: value}) class F(FilterSet): username = CharFilter(method=filter_username) class Meta: model = User fields = ["username"] self.assertEqual(list(F().qs), list(User.objects.all())) self.assertEqual( list(F({"username": "alex"}).qs), [User.objects.get(username="alex")] ) self.assertEqual(list(F({"username": "jose"}).qs), list()) class O2ORelationshipTests(TestCase): def setUp(self): a1 = Account.objects.create( name="account1", in_good_standing=False, friendly=False ) a2 = Account.objects.create( name="account2", in_good_standing=True, friendly=True ) a3 = Account.objects.create( name="account3", in_good_standing=True, friendly=False ) a4 = Account.objects.create( name="account4", in_good_standing=False, friendly=True ) Profile.objects.create(account=a1, likes_coffee=True, likes_tea=False) Profile.objects.create(account=a2, likes_coffee=False, likes_tea=True) Profile.objects.create(account=a3, likes_coffee=True, likes_tea=True) Profile.objects.create(account=a4, likes_coffee=False, likes_tea=False) def test_o2o_relation(self): class F(FilterSet): class Meta: model = Profile fields = ("account",) f = F() self.assertEqual(f.qs.count(), 4) f = F({"account": 1}) self.assertEqual(f.qs.count(), 1) self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_o2o_relation_dictionary(self): class F(FilterSet): class Meta: model = Profile fields = { "account": ["exact"], } f = F() self.assertEqual(f.qs.count(), 4) f = F({"account": 1}) self.assertEqual(f.qs.count(), 1) self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_reverse_o2o_relation(self): class F(FilterSet): class Meta: model = Account fields = ("profile",) f = F() self.assertEqual(f.qs.count(), 4) f = F({"profile": 1}) self.assertEqual(f.qs.count(), 1) self.assertQuerySetEqual(f.qs, [1], lambda o: o.pk) def test_o2o_relation_attribute(self): class F(FilterSet): class Meta: model = Profile fields = ("account__in_good_standing",) f = F() self.assertEqual(f.qs.count(), 4) f = F({"account__in_good_standing": "2"}) self.assertEqual(f.qs.count(), 2) self.assertQuerySetEqual(f.qs, [2, 3], lambda o: o.pk, False) def test_o2o_relation_attribute2(self): class F(FilterSet): class Meta: model = Profile fields = ( "account__in_good_standing", "account__friendly", ) f = F() self.assertEqual(f.qs.count(), 4) f = F({"account__in_good_standing": "2", "account__friendly": "2"}) self.assertEqual(f.qs.count(), 1) self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) def test_reverse_o2o_relation_attribute(self): class F(FilterSet): class Meta: model = Account fields = ("profile__likes_coffee",) f = F() self.assertEqual(f.qs.count(), 4) f = F({"profile__likes_coffee": "2"}) self.assertEqual(f.qs.count(), 2) self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) def test_reverse_o2o_relation_attribute2(self): class F(FilterSet): class Meta: model = Account fields = ("profile__likes_coffee", "profile__likes_tea") f = F() self.assertEqual(f.qs.count(), 4) f = F({"profile__likes_coffee": "2", "profile__likes_tea": "2"}) self.assertEqual(f.qs.count(), 1) self.assertQuerySetEqual(f.qs, [3], lambda o: o.pk) class FKRelationshipTests(TestCase): def test_fk_relation(self): company1 = Company.objects.create(name="company1") company2 = Company.objects.create(name="company2") Location.objects.create(company=company1, open_days="some", zip_code="90210") Location.objects.create(company=company2, open_days="WEEKEND", zip_code="11111") Location.objects.create(company=company1, open_days="monday", zip_code="12345") class F(FilterSet): class Meta: model = Location fields = ("company",) f = F() self.assertEqual(f.qs.count(), 3) f = F({"company": 1}) self.assertEqual(f.qs.count(), 2) self.assertQuerySetEqual(f.qs, [1, 3], lambda o: o.pk, False) def test_reverse_fk_relation(self): alex = User.objects.create(username="alex") jacob = User.objects.create(username="jacob") date = now().date() time = now().time() Comment.objects.create(text="comment 1", author=jacob, time=time, date=date) Comment.objects.create(text="comment 2", author=alex, time=time, date=date) Comment.objects.create(text="comment 3", author=jacob, time=time, date=date) class F(FilterSet): class Meta: model = User fields = ["comments"] qs = User.objects.all() f = F({"comments": [2]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) class F(FilterSet): comments = AllValuesFilter() class Meta: model = User fields = ["comments"] f = F({"comments": 2}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) def test_fk_relation_attribute(self): now_dt = now() alex = User.objects.create(username="alex") jacob = User.objects.create(username="jacob") User.objects.create(username="aaron") Article.objects.create(author=alex, published=now_dt) Article.objects.create(author=jacob, published=now_dt) Article.objects.create(author=alex, published=now_dt) class F(FilterSet): class Meta: model = Article fields = ["author__username"] self.assertEqual(list(F.base_filters), ["author__username"]) self.assertEqual(F({"author__username": "alex"}).qs.count(), 2) self.assertEqual(F({"author__username": "jacob"}).qs.count(), 1) class F(FilterSet): author__username = AllValuesFilter() class Meta: model = Article fields = ["author__username"] self.assertEqual(F({"author__username": "alex"}).qs.count(), 2) def test_reverse_fk_relation_attribute(self): alex = User.objects.create(username="alex") jacob = User.objects.create(username="jacob") date = now().date() time = now().time() Comment.objects.create(text="comment 1", author=jacob, time=time, date=date) Comment.objects.create(text="comment 2", author=alex, time=time, date=date) Comment.objects.create(text="comment 3", author=jacob, time=time, date=date) class F(FilterSet): class Meta: model = User fields = ["comments__text"] qs = User.objects.all() f = F({"comments__text": "comment 2"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) class F(FilterSet): comments__text = AllValuesFilter() class Meta: model = User fields = ["comments__text"] f = F({"comments__text": "comment 2"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) @unittest.skip("todo - need correct models") def test_fk_relation_multiple_attributes(self): pass @unittest.expectedFailure def test_reverse_fk_relation_multiple_attributes(self): company = Company.objects.create(name="company") Location.objects.create(company=company, open_days="some", zip_code="90210") Location.objects.create(company=company, open_days="WEEKEND", zip_code="11111") class F(FilterSet): class Meta: model = Company fields = ("locations__zip_code", "locations__open_days") f = F({"locations__zip_code": "90210", "locations__open_days": "WEEKEND"}) self.assertEqual(f.qs.count(), 0) class M2MRelationshipTests(TestCase): def setUp(self): alex = User.objects.create(username="alex", status=1) User.objects.create(username="jacob", status=1) aaron = User.objects.create(username="aaron", status=1) b1 = Book.objects.create(title="Ender's Game", price="1.00", average_rating=3.0) b2 = Book.objects.create(title="Rainbow Six", price="2.00", average_rating=4.0) b3 = Book.objects.create(title="Snowcrash", price="1.00", average_rating=4.0) Book.objects.create( title="Stranger in a Strage Land", price="2.00", average_rating=3.0 ) alex.favorite_books.add(b1, b2) aaron.favorite_books.add(b1, b3) def test_m2m_relation(self): class F(FilterSet): class Meta: model = User fields = ["favorite_books"] qs = User.objects.all().order_by("username") f = F({"favorite_books": ["1"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["1", "3"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books": ["2"]}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) f = F({"favorite_books": ["4"]}, queryset=qs) self.assertQuerySetEqual(f.qs, [], lambda o: o.username) def test_reverse_m2m_relation(self): class F(FilterSet): class Meta: model = Book fields = ["lovers"] qs = Book.objects.all().order_by("title") f = F({"lovers": [1]}, queryset=qs) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) class F(FilterSet): lovers = AllValuesFilter() class Meta: model = Book fields = ["lovers"] f = F({"lovers": 1}, queryset=qs) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) def test_m2m_relation_attribute(self): class F(FilterSet): class Meta: model = User fields = ["favorite_books__title"] qs = User.objects.all().order_by("username") f = F({"favorite_books__title": "Ender's Game"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["aaron", "alex"], lambda o: o.username) f = F({"favorite_books__title": "Rainbow Six"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) # No choices given, so filtering does nothing. class F(FilterSet): favorite_books__title = MultipleChoiceFilter() class Meta: model = User fields = ["favorite_books__title"] f = F() self.assertEqual(len(f.filters["favorite_books__title"].field.choices), 0) # Specifying choices allows filter to work. (See also AllValues variants.) class F(FilterSet): favorite_books__title = MultipleChoiceFilter( choices=[ (b.title, b.title) for b in Book.objects.all() ] ) class Meta: model = User fields = ["favorite_books__title"] f = F({'favorite_books__title': ["Ender's Game", "Snowcrash"]}, queryset=qs) self.assertIs(True, f.form.is_valid(), list(f.filters["favorite_books__title"].field.choices)) self.assertQuerySetEqual( f.qs, ['aaron', 'alex'], lambda o: o.username, ) class F(FilterSet): favorite_books__title = AllValuesFilter() class Meta: model = User fields = ["favorite_books__title"] f = F({"favorite_books__title": "Snowcrash"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["aaron"], lambda o: o.username) def test_reverse_m2m_relation_attribute(self): class F(FilterSet): class Meta: model = Book fields = ["lovers__username"] qs = Book.objects.all().order_by("title") f = F({"lovers__username": "alex"}, queryset=qs) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) f = F({"lovers__username": "jacob"}, queryset=qs) self.assertQuerySetEqual(f.qs, [], lambda o: o.title) class F(FilterSet): lovers__username = MultipleChoiceFilter() class Meta: model = Book fields = ["lovers__username"] f = F() self.assertEqual(len(f.filters["lovers__username"].field.choices), 0) # f = F({'lovers__username': ['1', '3']}, # queryset=qs) # self.assertQuerySetEqual( # f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title) class F(FilterSet): lovers__username = AllValuesFilter() class Meta: model = Book fields = ["lovers__username"] f = F({"lovers__username": "alex"}, queryset=qs) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) @unittest.expectedFailure def test_m2m_relation_multiple_attributes(self): class F(FilterSet): class Meta: model = User fields = ["favorite_books__price", "favorite_books__average_rating"] qs = User.objects.all().order_by("username") f = F( {"favorite_books__price": "1.00", "favorite_books__average_rating": 4.0}, queryset=qs, ) self.assertQuerySetEqual(f.qs, ["aaron"], lambda o: o.username) f = F( {"favorite_books__price": "3.00", "favorite_books__average_rating": 4.0}, queryset=qs, ) self.assertQuerySetEqual(f.qs, [], lambda o: o.username) @unittest.expectedFailure def test_reverse_m2m_relation_multiple_attributes(self): class F(FilterSet): class Meta: model = Book fields = ["lovers__status", "lovers__username"] qs = Book.objects.all().order_by("title") f = F({"lovers__status": 1, "lovers__username": "alex"}, queryset=qs) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six"], lambda o: o.title ) f = F({"lovers__status": 1, "lovers__username": "jacob"}, queryset=qs) self.assertQuerySetEqual(f.qs, [], lambda o: o.title) @unittest.skip("todo") def test_fk_relation_on_m2m_relation(self): pass @unittest.skip("todo") def test_fk_relation_attribute_on_m2m_relation(self): pass class SymmetricalSelfReferentialRelationshipTests(TestCase): def setUp(self): n1 = Node.objects.create(name="one") n2 = Node.objects.create(name="two") n3 = Node.objects.create(name="three") n4 = Node.objects.create(name="four") n1.adjacents.add(n2) n2.adjacents.add(n3) n2.adjacents.add(n4) n4.adjacents.add(n1) def test_relation(self): class F(FilterSet): class Meta: model = Node fields = ["adjacents"] qs = Node.objects.all().order_by("pk") f = F({"adjacents": ["1"]}, queryset=qs) self.assertQuerySetEqual(f.qs, [2, 4], lambda o: o.pk) class NonSymmetricalSelfReferentialRelationshipTests(TestCase): def setUp(self): n1 = DirectedNode.objects.create(name="one") n2 = DirectedNode.objects.create(name="two") n3 = DirectedNode.objects.create(name="three") n4 = DirectedNode.objects.create(name="four") n1.outbound_nodes.add(n2) n2.outbound_nodes.add(n3) n2.outbound_nodes.add(n4) n4.outbound_nodes.add(n1) def test_forward_relation(self): class F(FilterSet): class Meta: model = DirectedNode fields = ["outbound_nodes"] qs = DirectedNode.objects.all().order_by("pk") f = F({"outbound_nodes": ["1"]}, queryset=qs) self.assertQuerySetEqual(f.qs, [4], lambda o: o.pk) def test_reverse_relation(self): class F(FilterSet): class Meta: model = DirectedNode fields = ["inbound_nodes"] qs = DirectedNode.objects.all().order_by("pk") f = F({"inbound_nodes": ["1"]}, queryset=qs) self.assertQuerySetEqual(f.qs, [2], lambda o: o.pk) @override_settings(TIME_ZONE="UTC") class TransformedQueryExpressionFilterTests(TestCase): def test_filtering(self): now_dt = now() after_5pm = now_dt.replace(hour=18) before_5pm = now_dt.replace(hour=16) u = User.objects.create(username="alex") a = Article.objects.create(author=u, published=after_5pm) Article.objects.create(author=u, published=before_5pm) class F(FilterSet): class Meta: model = Article fields = {"published": ["hour__gte"]} qs = Article.objects.all() f = F({"published__hour__gte": 17}, queryset=qs) self.assertEqual(len(f.qs), 1) self.assertQuerySetEqual(f.qs, [a.pk], lambda o: o.pk) class LookupChoiceFilterTests(TestCase): class BookFilter(FilterSet): price = LookupChoiceFilter( lookup_choices=["lt", "gt"], field_class=forms.DecimalField ) class Meta: model = Book fields = ["price"] @classmethod def setUpTestData(cls): Book.objects.create( title="Ender's Game", price="10.0", average_rating=4.7999999999999998 ) Book.objects.create( title="Rainbow Six", price="15.0", average_rating=4.5999999999999996 ) Book.objects.create( title="Snowcrash", price="20.0", average_rating=4.2999999999999998 ) def test_filtering(self): F = self.BookFilter f = F({"price": "15", "price_lookup": "lt"}) self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) f = F({"price": "15", "price_lookup": "lt"}) self.assertQuerySetEqual(f.qs, ["Ender's Game"], lambda o: o.title) f = F({"price": "", "price_lookup": "lt"}) self.assertTrue(f.is_valid()) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six", "Snowcrash"], lambda o: o.title, ordered=False, ) f = F({"price": "15"}) self.assertFalse(f.is_valid()) self.assertQuerySetEqual( f.qs, ["Ender's Game", "Rainbow Six", "Snowcrash"], lambda o: o.title, ordered=False, ) def test_inner_field_class_validation(self): f = self.BookFilter({"price": "asdf", "price_lookup": "lt"}) self.assertFalse(f.is_valid()) self.assertEqual( f.errors, { "price": ["Enter a number."], }, ) def test_lookup_choices_validation(self): f = self.BookFilter({"price": "1", "price_lookup": "asdf"}) self.assertFalse(f.is_valid()) self.assertEqual( f.errors, { "price": [ "Select a valid choice. asdf is not one of the available choices." ], }, ) def test_lookup_omitted(self): f = self.BookFilter({"price": "1"}) self.assertFalse(f.is_valid()) self.assertEqual( f.errors, { "price": ["Select a lookup."], }, ) @override_settings(TIME_ZONE="UTC") class CSVFilterTests(TestCase): def setUp(self): u1 = User.objects.create(username="alex", status=1) u2 = User.objects.create(username="jacob", status=2) User.objects.create(username="aaron", status=2) User.objects.create(username="carl", status=0) now_dt = now() after_5pm = now_dt.replace(hour=18) before_5pm = now_dt.replace(hour=16) Article.objects.create(author=u1, published=after_5pm) Article.objects.create(author=u2, published=after_5pm) Article.objects.create(author=u1, published=before_5pm) Article.objects.create(author=u2, published=before_5pm) class UserFilter(FilterSet): class Meta: model = User fields = { "username": ["in"], "status": ["in"], } class ArticleFilter(FilterSet): class Meta: model = Article fields = { "author": ["in"], "published": ["in"], } self.user_filter = UserFilter self.article_filter = ArticleFilter self.after_5pm = after_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") self.before_5pm = before_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") def test_numeric_filtering(self): F = self.user_filter qs = User.objects.order_by("pk") cases = [ (None, [1, 2, 3, 4]), (QueryDict("status__in=1&status__in=2"), [2, 3]), ({"status__in": ""}, [1, 2, 3, 4]), ({"status__in": ","}, []), ({"status__in": "0"}, [4]), ({"status__in": "0,2"}, [2, 3, 4]), ({"status__in": "0,,1"}, [1, 4]), ({"status__in": "2"}, [2, 3]), ] for params, expected in cases: with self.subTest(params=params, expected=expected): self.assertQuerySetEqual( F(params, queryset=qs).qs, expected, attrgetter("pk") ) def test_string_filtering(self): F = self.user_filter qs = User.objects.order_by("pk") cases = [ (None, [1, 2, 3, 4]), (QueryDict("username__in=alex&username__in=aaron"), [3]), ({"username__in": ""}, [1, 2, 3, 4]), ({"username__in": ","}, []), ({"username__in": "alex"}, [1]), ({"username__in": "alex,aaron"}, [1, 3]), ({"username__in": "alex,,aaron"}, [1, 3]), ({"username__in": "alex,"}, [1]), ] for params, expected in cases: with self.subTest(params=params, expected=expected): self.assertQuerySetEqual( F(params, queryset=qs).qs, expected, attrgetter("pk") ) def test_datetime_filtering(self): F = self.article_filter qs = Article.objects.order_by("pk") after = self.after_5pm before = self.before_5pm cases = [ (None, [1, 2, 3, 4]), (QueryDict("published__in=%s&published__in=%s" % (after, before)), [3, 4]), ({"published__in": ""}, [1, 2, 3, 4]), ({"published__in": ","}, []), ({"published__in": "%s" % (after,)}, [1, 2]), ( { "published__in": "%s,%s" % ( after, before, ) }, [1, 2, 3, 4], ), ( { "published__in": "%s,,%s" % ( after, before, ) }, [1, 2, 3, 4], ), ({"published__in": "%s," % (after,)}, [1, 2]), ] for params, expected in cases: with self.subTest(params=params, expected=expected): self.assertQuerySetEqual( F(params, queryset=qs).qs, expected, attrgetter("pk") ) def test_related_filtering(self): F = self.article_filter qs = Article.objects.order_by("pk") cases = [ (None, [1, 2, 3, 4]), (QueryDict("author__in=1&author__in=2"), [2, 4]), ({"author__in": ""}, [1, 2, 3, 4]), ({"author__in": ","}, []), ({"author__in": "1"}, [1, 3]), ({"author__in": "1,2"}, [1, 2, 3, 4]), ({"author__in": "1,,2"}, [1, 2, 3, 4]), ({"author__in": "1,"}, [1, 3]), ] for params, expected in cases: with self.subTest(params=params, expected=expected): self.assertQuerySetEqual( F(params, queryset=qs).qs, expected, attrgetter("pk") ) @override_settings(TIME_ZONE="UTC") class CSVRangeFilterTests(TestCase): class ArticleFilter(FilterSet): class Meta: model = Article fields = { "published": ["range"], } @classmethod def setUpTestData(cls): u1 = User.objects.create(username="alex", status=1) u2 = User.objects.create(username="jacob", status=2) User.objects.create(username="aaron", status=2) User.objects.create(username="carl", status=0) now_dt = now() after_5pm = now_dt.replace(hour=18) around_5pm = now_dt.replace(hour=17) before_5pm = now_dt.replace(hour=16) Article.objects.create(author=u1, published=after_5pm) Article.objects.create(author=u2, published=around_5pm) Article.objects.create(author=u1, published=around_5pm) Article.objects.create(author=u2, published=before_5pm) cls.after_5pm = after_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") cls.around_5pm = around_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") cls.before_5pm = before_5pm.strftime("%Y-%m-%d %H:%M:%S.%f") def test_filtering(self): F = self.ArticleFilter f = F() self.assertEqual(f.qs.count(), 4) # empty value is a noop f = F({"published__range": ""}) self.assertTrue(f.is_valid()) self.assertEqual(f.qs.count(), 4) # empty values are interpreted as None types f = F({"published__range": ","}) self.assertEqual(f.qs.count(), 0) f = F({"published__range": "%s" % (self.before_5pm,)}) self.assertFalse(f.is_valid()) f = F( { "published__range": "%s,%s" % ( self.before_5pm, self.around_5pm, ) } ) self.assertEqual(f.qs.count(), 3) f = F( { "published__range": "%s,,%s" % ( self.before_5pm, self.after_5pm, ) } ) self.assertFalse(f.is_valid()) # empty value is interpreted as None type f = F({"published__range": "%s," % (self.before_5pm,)}) self.assertEqual(f.qs.count(), 0) class OrderingFilterTests(TestCase): def setUp(self): User.objects.create(username="alex", status=1) User.objects.create(username="jacob", status=2) User.objects.create(username="aaron", status=2) User.objects.create(username="carl", status=0) def test_ordering(self): class F(FilterSet): o = OrderingFilter(fields=("username",)) class Meta: model = User fields = ["username"] qs = User.objects.all() tests = [ {"o": "username"}, QueryDict("o=username,"), ] for data in tests: with self.subTest(data=data): f = F(data, queryset=qs) names = f.qs.values_list("username", flat=True) self.assertEqual(list(names), ["aaron", "alex", "carl", "jacob"]) def test_ordering_with_select_widget(self): class F(FilterSet): o = OrderingFilter(widget=forms.Select, fields=("username",)) class Meta: model = User fields = ["username"] qs = User.objects.all() f = F({"o": "username"}, queryset=qs) names = f.qs.values_list("username", flat=True) self.assertEqual(list(names), ["aaron", "alex", "carl", "jacob"]) def test_csv_input(self): class F(FilterSet): o = OrderingFilter(widget=forms.Select, fields=("username",),) class Meta: model = User fields = ["username"] qs = User.objects.all() tests = [ {"o": ","}, QueryDict("o=%2c"), QueryDict("o=,"), ] for data in tests: with self.subTest(data=data): f = F(data, queryset=qs) self.assertIs(True, f.is_valid()) names = f.qs.values_list("username", flat=True) self.assertEqual(list(names), ['alex', 'jacob', 'aaron', 'carl']) class MiscFilterSetTests(TestCase): def setUp(self): User.objects.create(username="alex", last_name="johnson", status=1) User.objects.create(username="jacob", last_name="johnson", status=2) User.objects.create(username="aaron", last_name="white", status=2) User.objects.create(username="carl", last_name="black", status=0) def test_filtering_with_declared_filters(self): class F(FilterSet): account = CharFilter(field_name="username") class Meta: model = User fields = ["account"] qs = MockQuerySet() F({"account": "jdoe"}, queryset=qs).qs qs.all.return_value.filter.assert_called_with(username__exact="jdoe") def test_filtering_without_meta(self): class F(FilterSet): username = CharFilter() f = F({"username": "alex"}, queryset=User.objects.all()) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) def test_filtering_with_multiple_filters(self): class F(FilterSet): class Meta: model = User fields = ["status", "username"] qs = User.objects.all() f = F({"username": "alex", "status": "1"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["alex"], lambda o: o.username) f = F({"username": "alex", "status": "2"}, queryset=qs) self.assertQuerySetEqual(f.qs, [], lambda o: o.pk) def test_filter_with_initial(self): # Initial values are a form presentation option - the FilterSet should # not use an initial value as a default value to filter by. class F(FilterSet): status = ChoiceFilter(choices=STATUS_CHOICES, initial=1) class Meta: model = User fields = ["status"] qs = User.objects.all() users = ["alex", "jacob", "aaron", "carl"] f = F(queryset=qs) self.assertQuerySetEqual(f.qs.order_by("pk"), users, lambda o: o.username) f = F({"status": 0}, queryset=qs) self.assertQuerySetEqual(f.qs, ["carl"], lambda o: o.username) def test_qs_count(self): class F(FilterSet): class Meta: model = User fields = ["status"] qs = User.objects.all() f = F(queryset=qs) self.assertEqual(len(f.qs), 4) self.assertEqual(f.qs.count(), 4) f = F({"status": "0"}, queryset=qs) self.assertEqual(len(f.qs), 1) self.assertEqual(f.qs.count(), 1) f = F({"status": "1"}, queryset=qs) self.assertEqual(len(f.qs), 1) self.assertEqual(f.qs.count(), 1) f = F({"status": "2"}, queryset=qs) self.assertEqual(len(f.qs), 2) self.assertEqual(f.qs.count(), 2) def test_filtering_with_widgets(self): class CharInFilter(BaseInFilter, CharFilter): pass class F(FilterSet): last_name = CharInFilter(widget=QueryArrayWidget) username = CharInFilter() class Meta: model = User fields = ["last_name", "username"] qs = User.objects.all() f = F({"last_name": ["johnson"]}, queryset=qs) self.assertQuerySetEqual( f.qs, ["alex", "jacob"], lambda o: o.username, ordered=False ) f = F({"last_name": ["johnson"], "username": "carl"}, queryset=qs) self.assertQuerySetEqual(f.qs, [], lambda o: o.username, ordered=False) f = F({"last_name": ["johnson"], "username": "jacob"}, queryset=qs) self.assertQuerySetEqual(f.qs, ["jacob"], lambda o: o.username, ordered=False) f = F( {"last_name": ["johnson", "white"], "username": "jacob, carl, aaron"}, queryset=qs, ) self.assertQuerySetEqual( f.qs, ["jacob", "aaron"], lambda o: o.username, ordered=False ) django-filter-23.5/tests/utils.py0000644000175000017500000000057514533556010016735 0ustar carstencarstenfrom unittest import mock from django.db import models class QuerySet(models.QuerySet): def __bool__(self): return True class MockQuerySet: """ Generate a mock that is suitably similar to a QuerySet """ def __new__(self): m = mock.Mock(spec_set=QuerySet()) m.filter.return_value = m m.all.return_value = m return m django-filter-23.5/tests/settings.py0000644000175000017500000000157114533556010017432 0ustar carstencarsten# ensure package/conf is importable from django_filters.conf import DEFAULTS DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", }, } INSTALLED_APPS = ( "django.contrib.contenttypes", "django.contrib.staticfiles", "django.contrib.auth", "rest_framework", "django_filters", "tests.rest_framework", "tests", ) MIDDLEWARE = [] ROOT_URLCONF = "tests.urls" USE_TZ = True TIME_ZONE = "UTC" SECRET_KEY = "foobar" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, } ] STATIC_URL = "/static/" # XMLTestRunner output TEST_OUTPUT_DIR = ".xmlcoverage" # help verify that DEFAULTS is importable from conf. def FILTERS_VERBOSE_LOOKUPS(): return DEFAULTS["VERBOSE_LOOKUPS"] DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" django-filter-23.5/tests/test_filters.py0000644000175000017500000015636014533556010020310 0ustar carstencarstenimport inspect from collections import OrderedDict from datetime import date, datetime, time, timedelta from unittest import mock from django import forms from django.test import TestCase, override_settings from django.utils import translation from django.utils.translation import gettext as _ from django_filters import filters, widgets from django_filters.fields import ( BaseCSVField, DateRangeField, DateTimeRangeField, IsoDateTimeRangeField, Lookup, RangeField, TimeRangeField, ) from django_filters.filters import ( AllValuesFilter, BaseCSVFilter, BaseInFilter, BaseRangeFilter, BooleanFilter, CharFilter, ChoiceFilter, DateFilter, DateFromToRangeFilter, DateRangeFilter, DateTimeFilter, DateTimeFromToRangeFilter, DurationFilter, Filter, IsoDateTimeFromToRangeFilter, LookupChoiceFilter, ModelChoiceFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter, NumberFilter, NumericRangeFilter, OrderingFilter, RangeFilter, TimeFilter, TimeRangeFilter, TypedMultipleChoiceFilter, UUIDFilter, ) from tests.models import Book, User class ModuleImportTests(TestCase): def is_filter(self, name, value): return isinstance(value, type) and issubclass(value, Filter) def test_imports(self): # msg = "Expected `filters.%s` to be imported in `filters.__all__`" filter_classes = [ key for key, value in inspect.getmembers(filters) if isinstance(value, type) and issubclass(value, Filter) ] # sanity check self.assertIn("Filter", filter_classes) self.assertIn("BooleanFilter", filter_classes) for f in filter_classes: self.assertIn(f, filters.__all__) class FilterTests(TestCase): def test_creation(self): f = Filter() self.assertEqual(f.lookup_expr, "exact") self.assertEqual(f.exclude, False) def test_creation_order(self): f = Filter() f2 = Filter() self.assertTrue(f2.creation_counter > f.creation_counter) def test_default_field(self): f = Filter() field = f.field self.assertIsInstance(field, forms.Field) def test_field_with_single_lookup_expr(self): f = Filter(lookup_expr="iexact") field = f.field self.assertIsInstance(field, forms.Field) def test_field_params(self): with mock.patch.object(Filter, "field_class", spec=["__call__"]) as mocked: f = Filter(field_name="somefield", label="somelabel", widget="somewidget") f.field mocked.assert_called_once_with( required=False, label="somelabel", widget="somewidget" ) def test_field_extra_params(self): with mock.patch.object(Filter, "field_class", spec=["__call__"]) as mocked: f = Filter(someattr="someattr") f.field mocked.assert_called_once_with( required=mock.ANY, label=mock.ANY, someattr="someattr" ) def test_field_required_default(self): # filter form fields should not be required by default with mock.patch.object(Filter, "field_class", spec=["__call__"]) as mocked: f = Filter() f.field mocked.assert_called_once_with(required=False, label=mock.ANY) def test_filtering(self): qs = mock.Mock(spec=["filter"]) f = Filter() result = f.filter(qs, "value") qs.filter.assert_called_once_with(None__exact="value") self.assertNotEqual(qs, result) def test_filtering_exclude(self): qs = mock.Mock(spec=["filter", "exclude"]) f = Filter(exclude=True) result = f.filter(qs, "value") qs.exclude.assert_called_once_with(None__exact="value") self.assertNotEqual(qs, result) def test_filtering_uses_name(self): qs = mock.Mock(spec=["filter"]) f = Filter(field_name="somefield") f.filter(qs, "value") result = qs.filter.assert_called_once_with(somefield__exact="value") self.assertNotEqual(qs, result) def test_filtering_skipped_with_blank_value(self): qs = mock.Mock() f = Filter() result = f.filter(qs, "") self.assertListEqual(qs.method_calls, []) self.assertEqual(qs, result) def test_filtering_skipped_with_none_value(self): qs = mock.Mock() f = Filter() result = f.filter(qs, None) self.assertListEqual(qs.method_calls, []) self.assertEqual(qs, result) def test_filter_using_method(self): qs = mock.NonCallableMock(spec=[]) method = mock.Mock() f = Filter(method=method) result = f.filter(qs, "value") method.assert_called_once_with(qs, None, "value") self.assertNotEqual(qs, result) def test_filtering_uses_distinct(self): qs = mock.Mock(spec=["filter", "distinct"]) f = Filter(field_name="somefield", distinct=True) f.filter(qs, "value") result = qs.distinct.assert_called_once_with() self.assertNotEqual(qs, result) class CharFilterTests(TestCase): def test_default_field(self): f = CharFilter() field = f.field self.assertIsInstance(field, forms.CharField) class UUIDFilterTests(TestCase): def test_default_field(self): f = UUIDFilter() field = f.field self.assertIsInstance(field, forms.UUIDField) class BooleanFilterTests(TestCase): def test_default_field(self): f = BooleanFilter() field = f.field self.assertIsInstance(field, forms.NullBooleanField) def test_filtering(self): qs = mock.Mock(spec=["filter"]) f = BooleanFilter(field_name="somefield") result = f.filter(qs, True) qs.filter.assert_called_once_with(somefield__exact=True) self.assertNotEqual(qs, result) def test_filtering_exclude(self): qs = mock.Mock(spec=["exclude"]) f = BooleanFilter(field_name="somefield", exclude=True) result = f.filter(qs, True) qs.exclude.assert_called_once_with(somefield__exact=True) self.assertNotEqual(qs, result) def test_filtering_skipped_with_blank_value(self): qs = mock.Mock() f = BooleanFilter(field_name="somefield") result = f.filter(qs, "") self.assertListEqual(qs.method_calls, []) self.assertEqual(qs, result) def test_filtering_skipped_with_none_value(self): qs = mock.Mock() f = BooleanFilter(field_name="somefield") result = f.filter(qs, None) self.assertListEqual(qs.method_calls, []) self.assertEqual(qs, result) def test_filtering_lookup_expr(self): qs = mock.Mock(spec=["filter"]) f = BooleanFilter(field_name="somefield", lookup_expr="isnull") result = f.filter(qs, True) qs.filter.assert_called_once_with(somefield__isnull=True) self.assertNotEqual(qs, result) class ChoiceFilterTests(TestCase): def test_default_field(self): f = ChoiceFilter() field = f.field self.assertIsInstance(field, forms.ChoiceField) def test_empty_choice(self): # default value f = ChoiceFilter(choices=[("a", "a")]) self.assertEqual( list(f.field.choices), [ ("", "---------"), ("a", "a"), ], ) # set value, allow blank label f = ChoiceFilter(choices=[("a", "a")], empty_label="") self.assertEqual( list(f.field.choices), [ ("", ""), ("a", "a"), ], ) # disable empty choice w/ None f = ChoiceFilter(choices=[("a", "a")], empty_label=None) self.assertEqual( list(f.field.choices), [ ("a", "a"), ], ) def test_null_choice(self): # default is to be disabled f = ChoiceFilter( choices=[("a", "a")], ) self.assertEqual( list(f.field.choices), [ ("", "---------"), ("a", "a"), ], ) # set label, allow blank label f = ChoiceFilter(choices=[("a", "a")], null_label="") self.assertEqual( list(f.field.choices), [ ("", "---------"), ("null", ""), ("a", "a"), ], ) # set null value f = ChoiceFilter(choices=[("a", "a")], null_value="NULL", null_label="") self.assertEqual( list(f.field.choices), [ ("", "---------"), ("NULL", ""), ("a", "a"), ], ) # explicitly disable f = ChoiceFilter(choices=[("a", "a")], null_label=None) self.assertEqual( list(f.field.choices), [ ("", "---------"), ("a", "a"), ], ) def test_null_multiplechoice(self): # default is to be disabled f = MultipleChoiceFilter( choices=[("a", "a")], ) self.assertEqual( list(f.field.choices), [ ("a", "a"), ], ) # set label, allow blank label f = MultipleChoiceFilter(choices=[("a", "a")], null_label="") self.assertEqual( list(f.field.choices), [ ("null", ""), ("a", "a"), ], ) # set null value f = MultipleChoiceFilter(choices=[("a", "a")], null_value="NULL", null_label="") self.assertEqual( list(f.field.choices), [ ("NULL", ""), ("a", "a"), ], ) # explicitly disable f = MultipleChoiceFilter(choices=[("a", "a")], null_label=None) self.assertEqual( list(f.field.choices), [ ("a", "a"), ], ) @override_settings( FILTERS_EMPTY_CHOICE_LABEL="EMPTY LABEL", FILTERS_NULL_CHOICE_LABEL="NULL LABEL", FILTERS_NULL_CHOICE_VALUE="NULL VALUE", ) def test_settings_overrides(self): f = ChoiceFilter( choices=[("a", "a")], ) self.assertEqual( list(f.field.choices), [ ("", "EMPTY LABEL"), ("NULL VALUE", "NULL LABEL"), ("a", "a"), ], ) f = MultipleChoiceFilter( choices=[("a", "a")], ) self.assertEqual( list(f.field.choices), [ ("NULL VALUE", "NULL LABEL"), ("a", "a"), ], ) def test_callable_choices(self): def choices(): yield ("a", "a") yield ("b", "b") f = ChoiceFilter(choices=choices) self.assertEqual( list(f.field.choices), [ ("", "---------"), ("a", "a"), ("b", "b"), ], ) def test_callable_choices_is_lazy(self): def choices(): self.fail("choices should not be called during initialization") ChoiceFilter(choices=choices) class MultipleChoiceFilterTests(TestCase): def test_default_field(self): f = MultipleChoiceFilter() field = f.field self.assertIsInstance(field, forms.MultipleChoiceField) def test_filtering_requires_name(self): qs = mock.Mock(spec=["filter"]) f = MultipleChoiceFilter() with self.assertRaises(TypeError): f.filter(qs, ["value"]) def test_conjoined_default_value(self): f = MultipleChoiceFilter() self.assertFalse(f.conjoined) def test_conjoined_true(self): f = MultipleChoiceFilter(conjoined=True) self.assertTrue(f.conjoined) def test_is_noop_false(self): f = MultipleChoiceFilter(required=False) f.always_filter = False self.assertFalse(f.is_noop(None, ["value"])) def test_filtering(self): qs = mock.Mock(spec=["filter"]) f = MultipleChoiceFilter(field_name="somefield") with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] f.filter(qs, ["value"]) self.assertEqual( mockQclass.call_args_list, [mock.call(), mock.call(somefield="value")] ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.filter.assert_called_once_with(mockQ1.__ior__.return_value) qs.filter.return_value.distinct.assert_called_once_with() def test_filtering_exclude(self): qs = mock.Mock(spec=["exclude"]) f = MultipleChoiceFilter(field_name="somefield", exclude=True) with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] f.filter(qs, ["value"]) self.assertEqual( mockQclass.call_args_list, [mock.call(), mock.call(somefield="value")] ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.exclude.assert_called_once_with(mockQ1.__ior__.return_value) qs.exclude.return_value.distinct.assert_called_once_with() def test_filtering_with_lookup_expr(self): qs = mock.Mock(spec=["filter"]) f = MultipleChoiceFilter(field_name="somefield", lookup_expr="icontains") with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] f.filter(qs, ["value"]) self.assertEqual( mockQclass.call_args_list, [mock.call(), mock.call(somefield__icontains="value")], ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.filter.assert_called_once_with(mockQ1.__ior__.return_value) qs.filter.return_value.distinct.assert_called_once_with() def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices( self, ): qs = mock.Mock(spec=[]) f = MultipleChoiceFilter(field_name="somefield", required=True) f.always_filter = False result = f.filter(qs, []) self.assertEqual(len(f.field.choices), 0) self.assertEqual(qs, result) f.field.choices = ["some", "values", "here"] result = f.filter(qs, ["some", "values", "here"]) self.assertEqual(qs, result) result = f.filter(qs, ["other", "values", "there"]) self.assertEqual(qs, result) def test_filtering_skipped_with_empty_list_value_and_some_choices(self): qs = mock.Mock(spec=[]) f = MultipleChoiceFilter(field_name="somefield") f.field.choices = ["some", "values", "here"] result = f.filter(qs, []) self.assertEqual(qs, result) def test_filter_conjoined_true(self): """Tests that a filter with `conjoined=True` returns objects that have all the values included in `value`. For example filter users that have all of this books. """ book_kwargs = {"price": 1, "average_rating": 1} books = [] books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) user1 = User.objects.create() user2 = User.objects.create() user3 = User.objects.create() user4 = User.objects.create() user5 = User.objects.create() user1.favorite_books.add(books[0], books[1]) user2.favorite_books.add(books[0], books[1], books[2]) user3.favorite_books.add(books[1], books[2]) user4.favorite_books.add(books[2], books[3]) user5.favorite_books.add(books[4], books[5]) filter_list = ( ( (books[0].pk, books[0].pk), # values [1, 2], ), # list of user.pk that have `value` books ((books[1].pk, books[1].pk), [1, 2, 3]), ((books[2].pk, books[2].pk), [2, 3, 4]), ( (books[3].pk, books[3].pk), [ 4, ], ), ( (books[4].pk, books[4].pk), [ 5, ], ), ((books[0].pk, books[1].pk), [1, 2]), ( (books[0].pk, books[2].pk), [ 2, ], ), ((books[1].pk, books[2].pk), [2, 3]), ( (books[2].pk, books[3].pk), [ 4, ], ), ( (books[4].pk, books[5].pk), [ 5, ], ), ((books[3].pk, books[4].pk), []), ) users = User.objects.all() for item in filter_list: f = MultipleChoiceFilter(field_name="favorite_books__pk", conjoined=True) queryset = f.filter(users, item[0]) expected_pks = [c[0] for c in queryset.values_list("pk")] self.assertListEqual( expected_pks, item[1], "Lists Differ: {0} != {1} for case {2}".format( expected_pks, item[1], item[0] ), ) class TypedMultipleChoiceFilterTests(TestCase): def test_default_field(self): f = TypedMultipleChoiceFilter() field = f.field self.assertIsInstance(field, forms.TypedMultipleChoiceField) def test_filtering_requires_name(self): qs = mock.Mock(spec=["filter"]) f = TypedMultipleChoiceFilter() with self.assertRaises(TypeError): f.filter(qs, ["value"]) def test_conjoined_default_value(self): f = TypedMultipleChoiceFilter() self.assertFalse(f.conjoined) def test_conjoined_true(self): f = TypedMultipleChoiceFilter(conjoined=True) self.assertTrue(f.conjoined) def test_filtering(self): qs = mock.Mock(spec=["filter"]) f = TypedMultipleChoiceFilter(field_name="somefield") with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] f.filter(qs, ["value"]) self.assertEqual( mockQclass.call_args_list, [mock.call(), mock.call(somefield="value")] ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.filter.assert_called_once_with(mockQ1.__ior__.return_value) qs.filter.return_value.distinct.assert_called_once_with() def test_filtering_exclude(self): qs = mock.Mock(spec=["exclude"]) f = TypedMultipleChoiceFilter(field_name="somefield", exclude=True) with mock.patch("django_filters.filters.Q") as mockQclass: mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock() mockQclass.side_effect = [mockQ1, mockQ2] f.filter(qs, ["value"]) self.assertEqual( mockQclass.call_args_list, [mock.call(), mock.call(somefield="value")] ) mockQ1.__ior__.assert_called_once_with(mockQ2) qs.exclude.assert_called_once_with(mockQ1.__ior__.return_value) qs.exclude.return_value.distinct.assert_called_once_with() def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices( self, ): qs = mock.Mock(spec=[]) f = TypedMultipleChoiceFilter(field_name="somefield", required=True) f.always_filter = False result = f.filter(qs, []) self.assertEqual(len(f.field.choices), 0) self.assertEqual(qs, result) f.field.choices = ["some", "values", "here"] result = f.filter(qs, ["some", "values", "here"]) self.assertEqual(qs, result) result = f.filter(qs, ["other", "values", "there"]) self.assertEqual(qs, result) def test_filtering_skipped_with_empty_list_value_and_some_choices(self): qs = mock.Mock(spec=[]) f = TypedMultipleChoiceFilter(field_name="somefield") f.field.choices = ["some", "values", "here"] result = f.filter(qs, []) self.assertEqual(qs, result) def test_filter_conjoined_true(self): """Tests that a filter with `conjoined=True` returns objects that have all the values included in `value`. For example filter users that have all of this books. """ book_kwargs = {"price": 1, "average_rating": 1} books = [] books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) books.append(Book.objects.create(**book_kwargs)) user1 = User.objects.create() user2 = User.objects.create() user3 = User.objects.create() user4 = User.objects.create() user5 = User.objects.create() user1.favorite_books.add(books[0], books[1]) user2.favorite_books.add(books[0], books[1], books[2]) user3.favorite_books.add(books[1], books[2]) user4.favorite_books.add(books[2], books[3]) user5.favorite_books.add(books[4], books[5]) filter_list = ( ( (books[0].pk, books[0].pk), # values [1, 2], ), # list of user.pk that have `value` books ((books[1].pk, books[1].pk), [1, 2, 3]), ((books[2].pk, books[2].pk), [2, 3, 4]), ( (books[3].pk, books[3].pk), [ 4, ], ), ( (books[4].pk, books[4].pk), [ 5, ], ), ((books[0].pk, books[1].pk), [1, 2]), ( (books[0].pk, books[2].pk), [ 2, ], ), ((books[1].pk, books[2].pk), [2, 3]), ( (books[2].pk, books[3].pk), [ 4, ], ), ( (books[4].pk, books[5].pk), [ 5, ], ), ((books[3].pk, books[4].pk), []), ) users = User.objects.all() for item in filter_list: f = TypedMultipleChoiceFilter( field_name="favorite_books__pk", conjoined=True ) queryset = f.filter(users, item[0]) expected_pks = [c[0] for c in queryset.values_list("pk")] self.assertListEqual( expected_pks, item[1], "Lists Differ: {0} != {1} for case {2}".format( expected_pks, item[1], item[0] ), ) class DateFilterTests(TestCase): def test_default_field(self): f = DateFilter() field = f.field self.assertIsInstance(field, forms.DateField) class DateTimeFilterTests(TestCase): def test_default_field(self): f = DateTimeFilter() field = f.field self.assertIsInstance(field, forms.DateTimeField) class TimeFilterTests(TestCase): def test_default_field(self): f = TimeFilter() field = f.field self.assertIsInstance(field, forms.TimeField) class DurationFilterTests(TestCase): def test_default_field(self): f = DurationFilter() field = f.field self.assertIsInstance(field, forms.DurationField) class MockQuerySetMixin: def get_mock_queryset(self): """ Configure mock QuerySet with required methods. """ qs = mock.NonCallableMock(spec=[]) qs.all = mock.Mock(return_value=qs) return qs class ModelChoiceFilterTests(TestCase, MockQuerySetMixin): def test_default_field_without_queryset(self): f = ModelChoiceFilter() with self.assertRaises(TypeError): f.field @override_settings( FILTERS_EMPTY_CHOICE_LABEL="EMPTY", FILTERS_NULL_CHOICE_VALUE="NULL", ) def test_empty_choices(self): f = ModelChoiceFilter( queryset=User.objects.all(), null_value="null", null_label="NULL" ) self.assertEqual( list(f.field.choices), [ ("", "EMPTY"), ("null", "NULL"), ], ) def test_default_field_with_queryset(self): qs = self.get_mock_queryset() f = ModelChoiceFilter(queryset=qs) field = f.field self.assertIsInstance(field, forms.ModelChoiceField) self.assertEqual(field.queryset, qs) def test_callable_queryset(self): request = mock.NonCallableMock(spec=[]) qs = self.get_mock_queryset() qs_callable = mock.Mock(return_value=qs) f = ModelChoiceFilter(queryset=qs_callable) f.parent = mock.Mock(request=request) field = f.field qs_callable.assert_called_with(request) self.assertEqual(field.queryset, qs) def test_get_queryset_override(self): request = mock.NonCallableMock(spec=[]) qs = self.get_mock_queryset() class F(ModelChoiceFilter): get_queryset = mock.create_autospec( ModelChoiceFilter.get_queryset, return_value=qs ) f = F() f.parent = mock.Mock(request=request) field = f.field f.get_queryset.assert_called_with(f, request) self.assertEqual(field.queryset, qs) class ModelMultipleChoiceFilterTests(TestCase, MockQuerySetMixin): def test_default_field_without_queryset(self): f = ModelMultipleChoiceFilter() with self.assertRaises(TypeError): f.field @override_settings( FILTERS_EMPTY_CHOICE_LABEL="EMPTY", FILTERS_NULL_CHOICE_VALUE="NULL", ) def test_empty_choices(self): f = ModelMultipleChoiceFilter( queryset=User.objects.all(), null_value="null", null_label="NULL" ) self.assertEqual( list(f.field.choices), [ ("null", "NULL"), ], ) def test_default_field_with_queryset(self): qs = self.get_mock_queryset() f = ModelMultipleChoiceFilter(queryset=qs) field = f.field self.assertIsInstance(field, forms.ModelMultipleChoiceField) self.assertEqual(field.queryset, qs) def test_filtering_to_field_name(self): qs = User.objects.all() f = ModelMultipleChoiceFilter( field_name="first_name", to_field_name="first_name", queryset=qs ) user = User.objects.create(first_name="Firstname") self.assertEqual(f.get_filter_predicate(user), {"first_name": "Firstname"}) self.assertEqual( f.get_filter_predicate("FilterValue"), {"first_name": "FilterValue"} ) self.assertEqual(list(f.filter(qs, ["Firstname"])), [user]) self.assertEqual(list(f.filter(qs, [user])), [user]) def test_callable_queryset(self): request = mock.NonCallableMock(spec=[]) qs = self.get_mock_queryset() qs_callable = mock.Mock(return_value=qs) f = ModelMultipleChoiceFilter(queryset=qs_callable) f.parent = mock.Mock(request=request) field = f.field qs_callable.assert_called_with(request) self.assertEqual(field.queryset, qs) class NumberFilterTests(TestCase): def test_default_field(self): f = NumberFilter() field = f.field self.assertIsInstance(field, forms.DecimalField) def test_filtering(self): qs = mock.Mock(spec=["filter"]) f = NumberFilter() f.filter(qs, 1) qs.filter.assert_called_once_with(None__exact=1) # Also test 0 as it once had a bug qs.reset_mock() f.filter(qs, 0) qs.filter.assert_called_once_with(None__exact=0) def test_filtering_exclude(self): qs = mock.Mock(spec=["exclude"]) f = NumberFilter(exclude=True) f.filter(qs, 1) qs.exclude.assert_called_once_with(None__exact=1) # Also test 0 as it once had a bug qs.reset_mock() f.filter(qs, 0) qs.exclude.assert_called_once_with(None__exact=0) class NumericRangeFilterTests(TestCase): def test_default_field(self): f = NumericRangeFilter() field = f.field self.assertIsInstance(field, RangeField) def test_filtering(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=20, stop=30) f = NumericRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__exact=(20, 30)) def test_filtering_exclude(self): qs = mock.Mock(spec=["exclude"]) value = mock.Mock(start=20, stop=30) f = NumericRangeFilter(exclude=True) f.filter(qs, value) qs.exclude.assert_called_once_with(None__exact=(20, 30)) def test_filtering_skipped_with_none_value(self): qs = mock.Mock(spec=["filter"]) f = NumericRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) def test_field_with_lookup_expr(self): qs = mock.Mock() value = mock.Mock(start=20, stop=30) f = NumericRangeFilter(lookup_expr=("overlap")) f.filter(qs, value) qs.filter.assert_called_once_with(None__overlap=(20, 30)) def test_zero_to_zero(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=0, stop=0) f = NumericRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__exact=(0, 0)) def test_filtering_startswith(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=20, stop=None) f = NumericRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__startswith=20) def test_filtering_endswith(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=30) f = NumericRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__endswith=30) def test_filtering_distinct(self): f = NumericRangeFilter(distinct=True) # range qs = mock.Mock() f.filter(qs, mock.Mock(start=20, stop=30)) qs.distinct.assert_called_once() qs.distinct.return_value.filter.assert_called_once_with(None__exact=(20, 30)) # min qs = mock.Mock() f.filter(qs, mock.Mock(start=20, stop=None)) qs.distinct.assert_called_once() qs.distinct.return_value.filter.assert_called_once_with(None__startswith=20) # max qs = mock.Mock() f.filter(qs, mock.Mock(start=None, stop=30)) qs.distinct.assert_called_once() qs.distinct.return_value.filter.assert_called_once_with(None__endswith=30) class RangeFilterTests(TestCase): def test_default_field(self): f = RangeFilter() field = f.field self.assertIsInstance(field, RangeField) def test_filtering_range(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=20, stop=30) f = RangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__range=(20, 30)) def test_filtering_exclude(self): qs = mock.Mock(spec=["exclude"]) value = mock.Mock(start=20, stop=30) f = RangeFilter(exclude=True) f.filter(qs, value) qs.exclude.assert_called_once_with(None__range=(20, 30)) def test_filtering_start(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=20, stop=None) f = RangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=20) def test_filtering_stop(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=30) f = RangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=30) def test_filtering_skipped_with_none_value(self): qs = mock.Mock(spec=["filter"]) f = RangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock(start=20, stop=30) f = RangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with(None__range=(20, 30)) def test_filtering_distinct(self): f = RangeFilter(distinct=True) # range qs = mock.Mock() f.filter(qs, mock.Mock(start=20, stop=30)) qs.distinct.assert_called_once() qs.distinct.return_value.filter.assert_called_once_with(None__range=(20, 30)) # min qs = mock.Mock() f.filter(qs, mock.Mock(start=20, stop=None)) qs.distinct.assert_called_once() qs.distinct.return_value.filter.assert_called_once_with(None__gte=20) # max qs = mock.Mock() f.filter(qs, mock.Mock(start=None, stop=30)) qs.distinct.assert_called_once() qs.distinct.return_value.filter.assert_called_once_with(None__lte=30) class DateRangeFilterTests(TestCase): def test_creating(self): f = DateRangeFilter(empty_label=None) self.assertEqual(len(f.choices), 5) self.assertIs(f.choices, f.extra["choices"]) f = DateRangeFilter(empty_label=None, choices=[], filters=[]) self.assertEqual(f.choices, []) self.assertEqual(f.filters, []) self.assertEqual(len(f.choices), 0) self.assertIs(f.choices, f.extra["choices"]) def test_default_field(self): f = DateRangeFilter() field = f.field self.assertIsInstance(field, forms.ChoiceField) def test_filtering(self): # skip filtering, as it's an empty value qs = mock.Mock(spec=[]) f = DateRangeFilter() result = f.filter(qs, "") self.assertEqual(qs, result) def test_filtering_skipped_with_out_of_range_value(self): # Field validation should prevent this from occurring qs = mock.Mock(spec=[]) f = DateRangeFilter() with self.assertRaises(AssertionError): f.filter(qs, "tomorrow") def test_choices_and_filters_mismatch(self): msg = ( "Keys must be present in both 'choices' and 'filters'. Missing keys: 'a, b'" ) with self.assertRaisesMessage(AssertionError, msg): DateRangeFilter(choices=[("a", "a")], filters={"b": None}) def test_choices_with_optgroups_dont_mistmatch(self): DateRangeFilter( choices=[("group", ("a", "a")), ("b", "b")], filters={"a": None, "b": None} ) def test_filtering_for_this_year(self): qs = mock.Mock(spec=["filter"]) with mock.patch("django_filters.filters.now") as mock_now: now_dt = mock_now.return_value f = DateRangeFilter() f.filter(qs, "year") qs.filter.assert_called_once_with(None__year=now_dt.year) def test_filtering_for_this_month(self): qs = mock.Mock(spec=["filter"]) with mock.patch("django_filters.filters.now") as mock_now: now_dt = mock_now.return_value f = DateRangeFilter() f.filter(qs, "month") qs.filter.assert_called_once_with( None__year=now_dt.year, None__month=now_dt.month ) def test_filtering_for_7_days(self): qs = mock.Mock(spec=["filter"]) with mock.patch("django_filters.filters.now"), mock.patch( "django_filters.filters.timedelta" ) as mock_td, mock.patch("django_filters.filters._truncate") as mock_truncate: mock_d1, mock_d2 = mock.MagicMock(), mock.MagicMock() mock_truncate.side_effect = [mock_d1, mock_d2] f = DateRangeFilter() f.filter(qs, "week") self.assertEqual( mock_td.call_args_list, [mock.call(days=7), mock.call(days=1)] ) qs.filter.assert_called_once_with(None__lt=mock_d2, None__gte=mock_d1) def test_filtering_for_today(self): qs = mock.Mock(spec=["filter"]) with mock.patch("django_filters.filters.now") as mock_now: now_dt = mock_now.return_value f = DateRangeFilter() f.filter(qs, "today") qs.filter.assert_called_once_with( None__year=now_dt.year, None__month=now_dt.month, None__day=now_dt.day ) def test_filtering_for_yesterday(self): qs = mock.Mock(spec=["filter"]) with mock.patch("django_filters.filters.now") as mock_now: now_dt = mock_now.return_value f = DateRangeFilter() f.filter(qs, "yesterday") qs.filter.assert_called_once_with( None__year=(now_dt - timedelta(days=1)).year, None__month=(now_dt - timedelta(days=1)).month, None__day=(now_dt - timedelta(days=1)).day, ) class DateFromToRangeFilterTests(TestCase): def test_default_field(self): f = DateFromToRangeFilter() field = f.field self.assertIsInstance(field, DateRangeField) def test_filtering_range(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=date(2015, 4, 7), stop=date(2015, 9, 6)) f = DateFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with( None__range=(date(2015, 4, 7), date(2015, 9, 6)) ) def test_filtering_start(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=date(2015, 4, 7), stop=None) f = DateFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=date(2015, 4, 7)) def test_filtering_stop(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=date(2015, 9, 6)) f = DateFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=date(2015, 9, 6)) def test_filtering_skipped_with_none_value(self): qs = mock.Mock(spec=["filter"]) f = DateFromToRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock(start=date(2015, 4, 7), stop=date(2015, 9, 6)) f = DateFromToRangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with( None__range=(date(2015, 4, 7), date(2015, 9, 6)) ) class DateTimeFromToRangeFilterTests(TestCase): def test_default_field(self): f = DateTimeFromToRangeFilter() field = f.field self.assertIsInstance(field, DateTimeRangeField) def test_filtering_range(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock( start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45) ) f = DateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with( None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)) ) def test_filtering_start(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=datetime(2015, 4, 7, 8, 30), stop=None) f = DateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=datetime(2015, 4, 7, 8, 30)) def test_filtering_stop(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=datetime(2015, 9, 6, 11, 45)) f = DateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=datetime(2015, 9, 6, 11, 45)) def test_filtering_skipped_with_none_value(self): qs = mock.Mock(spec=["filter"]) f = DateTimeFromToRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock( start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45) ) f = DateTimeFromToRangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with( None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)) ) class IsoDateTimeFromToRangeFilterTests(TestCase): def test_default_field(self): f = IsoDateTimeFromToRangeFilter() field = f.field self.assertIsInstance(field, IsoDateTimeRangeField) def test_filtering_range(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock( start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45) ) f = IsoDateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with( None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)) ) def test_filtering_start(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=datetime(2015, 4, 7, 8, 30), stop=None) f = IsoDateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=datetime(2015, 4, 7, 8, 30)) def test_filtering_stop(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=datetime(2015, 9, 6, 11, 45)) f = IsoDateTimeFromToRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=datetime(2015, 9, 6, 11, 45)) def test_filtering_skipped_with_none_value(self): qs = mock.Mock(spec=["filter"]) f = IsoDateTimeFromToRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock( start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45) ) f = IsoDateTimeFromToRangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with( None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)) ) class TimeRangeFilterTests(TestCase): def test_default_field(self): f = TimeRangeFilter() field = f.field self.assertIsInstance(field, TimeRangeField) def test_filtering_range(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=time(10, 15), stop=time(12, 30)) f = TimeRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__range=(time(10, 15), time(12, 30))) def test_filtering_start(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=time(10, 15), stop=None) f = TimeRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__gte=time(10, 15)) def test_filtering_stop(self): qs = mock.Mock(spec=["filter"]) value = mock.Mock(start=None, stop=time(12, 30)) f = TimeRangeFilter() f.filter(qs, value) qs.filter.assert_called_once_with(None__lte=time(12, 30)) def test_filtering_skipped_with_none_value(self): qs = mock.Mock(spec=["filter"]) f = TimeRangeFilter() result = f.filter(qs, None) self.assertEqual(qs, result) def test_filtering_ignores_lookup_expr(self): qs = mock.Mock() value = mock.Mock(start=time(10, 15), stop=time(12, 30)) f = TimeRangeFilter(lookup_expr="gte") f.filter(qs, value) qs.filter.assert_called_once_with(None__range=(time(10, 15), time(12, 30))) class AllValuesFilterTests(TestCase): def test_default_field_without_assigning_model(self): f = AllValuesFilter() with self.assertRaises(AttributeError): f.field def test_default_field_with_assigning_model(self): mocked = mock.Mock() chained_call = ".".join( [ "_default_manager", "distinct.return_value", "order_by.return_value", "values_list.return_value", ] ) mocked.configure_mock(**{chained_call: iter([])}) f = AllValuesFilter() f.model = mocked field = f.field self.assertIsInstance(field, forms.ChoiceField) def test_empty_value_in_choices(self): f = AllValuesFilter(field_name="username") f.model = User self.assertEqual( list(f.field.choices), [ ("", "---------"), ], ) class LookupChoiceFilterTests(TestCase): def test_normalize_lookup_no_display_label(self): # display label has underscores replaced and is capitalized display_label = LookupChoiceFilter.normalize_lookup("has_key") self.assertEqual(display_label, ("has_key", "Has key")) def test_normalize_lookup_with_display_label(self): # display label is not transformed if provided display_label = LookupChoiceFilter.normalize_lookup(("equal", "equals")) self.assertEqual(display_label, ("equal", "equals")) def test_lookup_choices_default(self): # Lookup choices should default to the model field's registered lookups f = LookupChoiceFilter(field_name="username", lookup_choices=None) f.model = User choice_field = f.field.fields[1] self.assertEqual( len(choice_field.choices), len(User._meta.get_field("username").get_lookups()) + 1, ) field_choices = dict(choice_field.choices) self.assertEqual(field_choices["exact"], "Exact") self.assertEqual(field_choices["startswith"], "Startswith") def test_lookup_choices_list(self): f = LookupChoiceFilter( field_name="username", lookup_choices=["exact", "startswith", "has_key"] ) choice_field = f.field.fields[1] self.assertEqual( list(choice_field.choices), [ ("", "---------"), ("exact", "Exact"), ("startswith", "Startswith"), ("has_key", "Has key"), ], ) def test_lookup_choices_pairs(self): f = LookupChoiceFilter( field_name="username", lookup_choices=[ ("exact", "Is equal to"), ("startswith", "Starts with"), ], ) choice_field = f.field.fields[1] self.assertEqual( list(choice_field.choices), [ ("", "---------"), ("exact", "Is equal to"), ("startswith", "Starts with"), ], ) def test_lookup_choices_empty_label_default(self): f = LookupChoiceFilter(field_name="username", lookup_choices=[]) choice_field = f.field.fields[1] self.assertEqual(list(choice_field.choices), [("", "---------")]) def test_lookup_choices_empty_label_disabled(self): f = LookupChoiceFilter( field_name="username", empty_label=None, lookup_choices=[] ) choice_field = f.field.fields[1] self.assertEqual(list(choice_field.choices), []) def test_filtering(self): qs = mock.Mock(spec=["filter"]) f = LookupChoiceFilter( field_name="somefield", lookup_choices=["some_lookup_expr"] ) result = f.filter(qs, Lookup("value", "some_lookup_expr")) qs.filter.assert_called_once_with(somefield__some_lookup_expr="value") self.assertNotEqual(qs, result) class CSVFilterTests(TestCase): def setUp(self): class NumberInFilter(BaseCSVFilter, NumberFilter): pass class DateTimeYearInFilter(BaseCSVFilter, DateTimeFilter): pass self.number_in = NumberInFilter(lookup_expr="in") self.datetimeyear_in = DateTimeYearInFilter(lookup_expr="year__in") def test_default_field(self): f = BaseCSVFilter() field = f.field self.assertIsInstance(field, forms.Field) def test_concrete_field(self): field = self.number_in.field self.assertIsInstance(field, forms.DecimalField) self.assertIsInstance(field, BaseCSVField) self.assertEqual(field.__class__.__name__, "DecimalInField") field = self.datetimeyear_in.field self.assertIsInstance(field, forms.DateTimeField) self.assertIsInstance(field, BaseCSVField) self.assertEqual(field.__class__.__name__, "DateTimeYearInField") def test_filtering(self): qs = mock.Mock(spec=["filter"]) f = self.number_in f.filter(qs, [1, 2]) qs.filter.assert_called_once_with(None__in=[1, 2]) def test_filtering_skipped_with_none_value(self): qs = mock.Mock(spec=["filter"]) f = self.number_in result = f.filter(qs, None) self.assertEqual(qs, result) def test_field_with_lookup_expr(self): qs = mock.Mock() f = self.datetimeyear_in f.filter(qs, [1, 2]) qs.filter.assert_called_once_with(None__year__in=[1, 2]) class BaseInFilterTests(TestCase): def test_filtering(self): class NumberInFilter(BaseInFilter, NumberFilter): pass qs = mock.Mock(spec=["filter"]) f = NumberInFilter() f.filter(qs, [1, 2]) qs.filter.assert_called_once_with(None__in=[1, 2]) class BaseRangeFilterTests(TestCase): def test_filtering(self): class NumberInFilter(BaseRangeFilter, NumberFilter): pass qs = mock.Mock(spec=["filter"]) f = NumberInFilter() f.filter(qs, [1, 2]) qs.filter.assert_called_once_with(None__range=[1, 2]) class OrderingFilterTests(TestCase): def test_default_field(self): f = OrderingFilter() field = f.field self.assertIsInstance(field, forms.ChoiceField) def test_filtering(self): qs = mock.Mock(spec=["order_by"]) f = OrderingFilter() f.filter(qs, ["a", "b"]) qs.order_by.assert_called_once_with("a", "b") def test_filtering_descending(self): qs = mock.Mock(spec=["order_by"]) f = OrderingFilter() f.filter(qs, ["-a"]) qs.order_by.assert_called_once_with("-a") def test_filtering_with_fields(self): qs = mock.Mock(spec=["order_by"]) f = OrderingFilter(fields={"a": "b"}) f.filter(qs, ["b", "-b"]) qs.order_by.assert_called_once_with("a", "-a") def test_filtering_skipped_with_none_value(self): qs = mock.Mock(spec=["order_by"]) f = OrderingFilter() result = f.filter(qs, None) self.assertEqual(qs, result) def test_choices_unaltered(self): # provided 'choices' should not be altered when 'fields' is present f = OrderingFilter( choices=(("a", "A"), ("b", "B")), fields=(("a", "c"), ("b", "d")), ) self.assertSequenceEqual( list(f.field.choices), ( ("", "---------"), ("a", "A"), ("b", "B"), ), ) def test_choices_from_fields(self): f = OrderingFilter( fields=(("a", "c"), ("b", "d")), ) self.assertSequenceEqual( list(f.field.choices), ( ("", "---------"), ("c", "C"), ("-c", "C (descending)"), ("d", "D"), ("-d", "D (descending)"), ), ) def test_field_labels(self): f = OrderingFilter( fields=(("a", "c"), ("b", "d")), field_labels={"a": "foo"}, ) self.assertSequenceEqual( list(f.field.choices), ( ("", "---------"), ("c", "foo"), ("-c", "foo (descending)"), ("d", "D"), ("-d", "D (descending)"), ), ) def test_field_labels_descending(self): f = OrderingFilter( fields=["username"], field_labels={ "username": "BLABLA", "-username": "XYZXYZ", }, ) self.assertEqual( list(f.field.choices), [ ("", "---------"), ("username", "BLABLA"), ("-username", "XYZXYZ"), ], ) def test_normalize_fields(self): f = OrderingFilter.normalize_fields O = OrderedDict # noqa self.assertIn("a", f({"a": "b"})) self.assertEqual(f(O([("a", "b"), ("c", "d")])), O([("a", "b"), ("c", "d")])) self.assertEqual(f([("a", "b"), ("c", "d")]), O([("a", "b"), ("c", "d")])) self.assertEqual(f(["a", "b"]), O([("a", "a"), ("b", "b")])) with self.assertRaises(AssertionError) as ctx: f(None) self.assertEqual( str(ctx.exception), "'fields' must be an iterable (e.g., a list, tuple, or mapping).", ) with self.assertRaises(AssertionError) as ctx: f([("a", "b", "c")]) self.assertEqual( str(ctx.exception), "'fields' must contain strings or (field name, param name) pairs.", ) with self.assertRaises(AssertionError) as ctx: f([0, 1, 2]) self.assertEqual( str(ctx.exception), "'fields' must contain strings or (field name, param name) pairs.", ) def test_widget(self): f = OrderingFilter() widget = f.field.widget self.assertIsInstance(widget, widgets.BaseCSVWidget) self.assertIsInstance(widget, forms.Select) def test_translation_sanity(self): with translation.override("pl"): self.assertEqual(_("Username"), "Nazwa użytkownika") self.assertEqual( _("%s (descending)") % _("Username"), "Nazwa użytkownika (malejąco)" ) def test_translation_default_label(self): with translation.override("pl"): f = OrderingFilter(fields=["username"]) self.assertEqual( list(f.field.choices), [ ("", "---------"), ("username", "Nazwa użytkownika"), ("-username", "Nazwa użytkownika (malejąco)"), ], ) def test_translation_override_label(self): with translation.override("pl"): f = OrderingFilter( fields=["username"], field_labels={"username": "BLABLA"}, ) self.assertEqual( list(f.field.choices), [ ("", "---------"), ("username", "BLABLA"), ("-username", "BLABLA (malejąco)"), ], ) def test_help_text(self): # regression test for #756 - the usual CSV help_text is not relevant to ordering filters. self.assertEqual(OrderingFilter().field.help_text, "") self.assertEqual(OrderingFilter(help_text="a").field.help_text, "a") django-filter-23.5/tests/test_widgets.py0000644000175000017500000004203414533556010020276 0ustar carstencarstenfrom django.forms import NumberInput, Select, TextInput from django.test import TestCase from django_filters.widgets import ( BaseCSVWidget, BooleanWidget, CSVWidget, LinkWidget, LookupChoiceWidget, QueryArrayWidget, RangeWidget, SuffixedMultiWidget, ) class LookupTypeWidgetTests(TestCase): def test_widget_requires_field(self): with self.assertRaises(TypeError): LookupChoiceWidget() def test_widget_render(self): widgets = [TextInput(), Select(choices=(("a", "a"), ("b", "b")))] w = LookupChoiceWidget(widgets) self.assertHTMLEqual( w.render("price", ""), """ """, ) self.assertHTMLEqual( w.render("price", None), """ """, ) self.assertHTMLEqual( w.render("price", ["2", "a"]), """ """, ) class LinkWidgetTests(TestCase): def test_widget_without_choices(self): w = LinkWidget() self.assertEqual(len(w.choices), 0) self.assertHTMLEqual(w.render("price", ""), """
      """) def test_widget(self): choices = ( ("test-val1", "test-label1"), ("test-val2", "test-label2"), ) w = LinkWidget(choices=choices) self.assertEqual(len(w.choices), 2) self.assertHTMLEqual( w.render("price", ""), """ """, ) self.assertHTMLEqual( w.render("price", None), """ """, ) self.assertHTMLEqual( w.render("price", "test-val1"), """ """, ) def test_widget_with_option_groups(self): choices = ( ( "Audio", ( ("vinyl", "Vinyl"), ("cd", "CD"), ), ), ( "Video", ( ("vhs", "VHS Tape"), ("dvd", "DVD"), ), ), ("unknown", "Unknown"), ) w = LinkWidget(choices=choices) self.assertHTMLEqual( w.render("media", ""), """ """, ) def test_widget_with_blank_choice(self): choices = ( ("", "---------"), ("test-val1", "test-label1"), ("test-val2", "test-label2"), ) w = LinkWidget(choices=choices) self.assertHTMLEqual( w.render("price", ""), """ """, ) def test_widget_value_from_datadict(self): w = LinkWidget() data = {"price": "test-val1"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, "test-val1") class SuffixedMultiWidgetTests(TestCase): def test_assertions(self): # number of widgets must match suffixes with self.assertRaises(AssertionError): SuffixedMultiWidget(widgets=[BooleanWidget]) # suffixes must be unique class W(SuffixedMultiWidget): suffixes = ["a", "a"] with self.assertRaises(AssertionError): W(widgets=[BooleanWidget, BooleanWidget]) # should succeed class W(SuffixedMultiWidget): suffixes = ["a", "b"] W(widgets=[BooleanWidget, BooleanWidget]) def test_render(self): class W(SuffixedMultiWidget): suffixes = ["min", "max"] w = W(widgets=[TextInput, TextInput]) self.assertHTMLEqual( w.render("price", ""), """ """, ) # blank suffix class W(SuffixedMultiWidget): suffixes = [None, "lookup"] w = W(widgets=[TextInput, TextInput]) self.assertHTMLEqual( w.render("price", ""), """ """, ) def test_value_from_datadict(self): class W(SuffixedMultiWidget): suffixes = ["min", "max"] w = W(widgets=[TextInput, TextInput]) result = w.value_from_datadict( { "price_min": "1", "price_max": "2", }, {}, "price", ) self.assertEqual(result, ["1", "2"]) result = w.value_from_datadict({}, {}, "price") self.assertEqual(result, [None, None]) # blank suffix class W(SuffixedMultiWidget): suffixes = ["", "lookup"] w = W(widgets=[TextInput, TextInput]) result = w.value_from_datadict( { "price": "1", "price_lookup": "lt", }, {}, "price", ) self.assertEqual(result, ["1", "lt"]) def test_value_omitted_from_data(self): class A(SuffixedMultiWidget): suffixes = ["b"] a = A(widgets=[BooleanWidget]) result = a.value_omitted_from_data([], None, "test") self.assertIsNotNone(result) def test_replace_name(self): class A(SuffixedMultiWidget): suffixes = ["test"] a = A(widgets=[None]) output = '
      ' index = 0 q = a.replace_name(output, index) self.assertEqual(q, '
      ') def test_decompress_value_none(self): class A(SuffixedMultiWidget): suffixes = [""] a = A(widgets=[None]) self.assertEqual(a.decompress(None), [None, None]) class RangeWidgetTests(TestCase): def test_widget(self): w = RangeWidget() self.assertEqual(len(w.widgets), 2) self.assertHTMLEqual( w.render("price", ""), """ - """, ) self.assertHTMLEqual( w.render("price", slice(5.99, 9.99)), """ - """, ) def test_widget_attributes(self): w = RangeWidget(attrs={"type": "date"}) self.assertEqual(len(w.widgets), 2) self.assertHTMLEqual( w.render("date", ""), """ - """, ) class BooleanWidgetTests(TestCase): def test_widget_render(self): w = BooleanWidget() self.assertHTMLEqual( w.render("price", ""), """ """, ) def test_widget_value_from_datadict(self): """ """ w = BooleanWidget() trueActive = {"active": "true"} result = w.value_from_datadict(trueActive, {}, "active") self.assertEqual(result, True) falseActive = {"active": "false"} result = w.value_from_datadict(falseActive, {}, "active") self.assertEqual(result, False) result = w.value_from_datadict({}, {}, "active") self.assertEqual(result, None) class BaseCSVWidgetTests(TestCase): def test_widget_render(self): class NumberCSVWidget(BaseCSVWidget, NumberInput): pass w = NumberCSVWidget(attrs={"test": "attr"}) self.assertHTMLEqual( w.render("price", None), """ """, ) self.assertHTMLEqual( w.render("price", ""), """ """, ) self.assertHTMLEqual( w.render("price", []), """ """, ) self.assertHTMLEqual( w.render("price", "1"), """ """, ) self.assertHTMLEqual( w.render("price", "1,2"), """ """, ) self.assertHTMLEqual( w.render("price", ["1", "2"]), """ """, ) self.assertHTMLEqual( w.render("price", [1, 2]), """ """, ) def test_widget_value_from_datadict(self): class NumberCSVWidget(BaseCSVWidget, NumberInput): pass w = NumberCSVWidget() data = {"price": None} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, None) data = {"price": "1"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["1"]) data = {"price": "1,2"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["1", "2"]) data = {"price": "1,,2"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["1", "", "2"]) data = {"price": "1,"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["1", ""]) data = {"price": ","} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["", ""]) data = {"price": ""} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) result = w.value_from_datadict({}, {}, "price") self.assertEqual(result, None) def test_surrogate_class(self): class ClassSurrogate(BaseCSVWidget, NumberInput): surrogate = NumberInput w = ClassSurrogate() self.assertIsInstance(w.surrogate, NumberInput) def test_surrogate_instance(self): class InstanceSurrogate(BaseCSVWidget, NumberInput): surrogate = NumberInput() w = InstanceSurrogate() self.assertIsInstance(w.surrogate, NumberInput) self.assertIsNot(InstanceSurrogate.surrogate, w.surrogate) # deepcopied class CSVWidgetTests(TestCase): def test_widget_render(self): w = CSVWidget(attrs={"test": "attr"}) self.assertHTMLEqual( w.render("price", None), """ """, ) self.assertHTMLEqual( w.render("price", ""), """ """, ) self.assertHTMLEqual( w.render("price", []), """ """, ) self.assertHTMLEqual( w.render("price", "1"), """ """, ) self.assertHTMLEqual( w.render("price", "1,2"), """ """, ) self.assertHTMLEqual( w.render("price", ["1", "2"]), """ """, ) self.assertHTMLEqual( w.render("price", [1, 2]), """ """, ) class CSVSelectTests(TestCase): class CSVSelect(BaseCSVWidget, Select): pass def test_widget(self): w = self.CSVSelect(choices=((1, "a"), (2, "b"))) self.assertHTMLEqual( w.render("price", None), """ """, ) self.assertHTMLEqual( w.render("price", ""), """ """, ) self.assertHTMLEqual( w.render("price", "1"), """ """, ) self.assertHTMLEqual( w.render("price", "1,2"), """ """, ) self.assertHTMLEqual( w.render("price", ["1", "2"]), """ """, ) self.assertHTMLEqual( w.render("price", [1, 2]), """ """, ) class QueryArrayWidgetTests(TestCase): def test_widget_value_from_datadict(self): w = QueryArrayWidget() # Values can be provided as csv string: ?foo=bar,baz data = {"price": None} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) data = {"price": "1"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["1"]) data = {"price": "1,2"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(sorted(result), ["1", "2"]) data = {"price": "1,,2"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(sorted(result), ["1", "2"]) data = {"price": "1,"} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["1"]) data = {"price": ","} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) data = {"price": ""} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) result = w.value_from_datadict({}, {}, "price") self.assertEqual(result, []) # Values can be provided as query array: ?foo[]=bar&foo[]=baz data = {"price[]": None} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) data = {"price[]": ["1"]} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["1"]) data = {"price[]": ["1", "2"]} result = w.value_from_datadict(data, {}, "price") self.assertEqual(sorted(result), ["1", "2"]) data = {"price[]": ["1", "", "2"]} result = w.value_from_datadict(data, {}, "price") self.assertEqual(sorted(result), ["1", "2"]) data = {"price[]": ["1", ""]} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, ["1"]) data = {"price[]": ["", ""]} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) data = {"price[]": []} result = w.value_from_datadict(data, {}, "price") self.assertEqual(result, []) result = w.value_from_datadict({}, {}, "price") self.assertEqual(result, []) django-filter-23.5/runshell.py0000755000175000017500000000060614533556010016265 0ustar carstencarsten#!/usr/bin/env python import os import sys from django.core.management import execute_from_command_line def runshell(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") execute_from_command_line(sys.argv[:1] + ["migrate", "--noinput", "-v", "0"]) execute_from_command_line(sys.argv[:1] + ["shell"] + sys.argv[1:]) if __name__ == "__main__": runshell() django-filter-23.5/tox.ini0000644000175000017500000000254714533556010015375 0ustar carstencarsten[tox] envlist = {py37,py38,py39,py310}-django32, {py38,py39}-{django40,django41,django42}, {py310, py311}-{django41,django42,latest}, {py310, py311, py312}-{django41,django42,django50,latest}, isort,lint,docs,warnings, isolated_build = true [latest] deps = https://github.com/django/django/archive/main.tar.gz https://github.com/encode/django-rest-framework/archive/master.tar.gz [testenv] commands = coverage run --parallel-mode --source django_filters ./runtests.py --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner {posargs} setenv = PYTHONDONTWRITEBYTECODE=1 deps = django32: django~=3.2.0 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 django42: Django>=4.2rc1,<5.0 django50: Django>=5.0b1,<5.1 !latest: djangorestframework latest: {[latest]deps} -r requirements/test-ci.txt [testenv:isort] commands = isort --check-only --diff django_filters tests {posargs} deps = isort [testenv:lint] commands = flake8 django_filters tests {posargs} deps = flake8 [testenv:docs] commands = sphinx-build -WE docs _docs deps = -rrequirements/docs.txt [testenv:warnings] ignore_outcome = True unignore_outcomes = True commands = python -Werror ./runtests.py --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner {posargs} deps = {[latest]deps} -rrequirements/test-ci.txt django-filter-23.5/codecov.yml0000644000175000017500000000011314533556010016212 0ustar carstencarstencoverage: status: project: false patch: false changes: false django-filter-23.5/.readthedocs.yaml0000644000175000017500000000053014533556010017277 0ustar carstencarsten# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.10" python: install: - requirements: requirements/docs.txt django-filter-23.5/CHANGES.rst0000644000175000017500000003557514533556010015673 0ustar carstencarstenVersion 23.5 (2023-12-05) ------------------------- * Fixed OrderingFilter handling of empty values. (#1628) Thanks to Matt Munns. Version 23.4 (2023-11-20) ------------------------- * Official support for Django 5.0 and Python 3.12. * Fix DeprecationWarning for pkgutil.find_loader. Thanks to `wmorrell`. * Adopted Furo theme for docs. Version 23.3 (2023-9-17) ------------------------ * Adds initial compatibility with Django 5.0, prior to Django 5.0a1. * Updates packaging to use pyproject.toml and Flit. Version 23.2 (2023-4-30) ------------------------ * Deprecated the schema generation methods of the DRF related ``DjangoFilterBackend``. These will be removed in version 25.1. You should use `drf-spectacular `_ for generating OpenAPI schemas with DRF. * In addition, stopped testing against the (very old now) ``coreapi`` schema generation. These methods should continue to work if you're using them until v25.1, but ``coreapi`` is no longer maintained, and is raising warnings against the current versions of Python. To workaround this is not worth the effort at this point. * Updated Polish translations. Version 23.1 (2023-3-26) ------------------------ * Declared support for Django 4.2. * Various updated and new translations. Thanks to all who contributed, and Weblate for hosting. * Fixed QueryArrayWidget.value_from_datadict() to not mutate input data. (#1540) Version 22.1 (2022-6-17) ------------------------ * Update supported Python and Django versions: minimal Python is now 3.7, minimum Django is now 3.2. * Added testing for Python 3.10 and Django 4.1. * Removed outdated deprecated warnings for code removed in version 2.1. * Removed `filter_class` (use `filterset_class`) and `filter_fields` (`filterset_fields`) that were deprecated in [version 2.0 (2018)](https://django-filter.readthedocs.io/en/main/guide/migration.html#view-attributes-renamed-867). * The code base is now formatted with Black. Version 21.1 (2021-9-24) ------------------------ This is a maintenance release updating CI testing for the latest non-end-of-life versions of Python and Django, and updating package metadata accordingly. With this release ``django-filter`` is switching to a two-part CalVer versioning scheme, such as ``21.1``. The first number is the year. The second is the release number within that year. On an on-going basis, Django-Filter aims to support all current Django versions, the matching current Python versions, and the latest version of Django REST Framework. Please see: * `Status of supported Python branches `_ * `List of supported Django versions `_ Support for Python and Django versions will be dropped when they reach end-of-life. Support for Python versions will dropped when they reach end-of-life, even when still supported by a current version of Django. Other breaking changes are rare. Where required, every effort will be made to apply a "Year plus two" deprecation period. For example, a change initially introduced in ``23.x`` would offer a fallback where feasible and finally be removed in ``25.1``. Where fallbacks are not feasible, breaking changes without deprecation will be called out in the release notes. Beyond that change, there are few changes. Some small bugfixes, improvements to localisation, and documentation tweaks. Thanks to all who were involved. Version 2.4.0 (2020-9-27) -------------------------- * SECURITY: Added a ``MaxValueValidator`` to the form field for ``NumberFilter``. This prevents a potential DoS attack if numbers with very large exponents were subsequently converted to integers. The default limit value for the validator is ``1e50``. The new ``NumberFilter.get_max_validator()`` allows customising the used validator, and may return ``None`` to disable the validation entirely. * Added testing against Django 3.1 and Python 3.9. In addition tests against Django main development branch are now required to pass. Version 2.3.0 (2020-6-5) ------------------------ * Fixed import of FieldDoesNotExist. (#1127) * Added testing against Django 3.0. (#1125) * Declared support for, and added testing against, Python 3.8. (#1138) * Fix filterset multiple inheritance bug (#1131) * Allowed customising default lookup expression. (#1129) * Drop Django 2.1 and below (#1180) * Fixed IsoDateTimeRangeFieldTests for Django 3.1 * Require tests to pass against Django `master`. Version 2.2 (2019-7-16) ----------------------- * Added ``DjangoFilterBackend.get_schema_operation_parameters()`` for DRF 3.10+ OpenAPI schema generation. (#1086) * Added ``lookup_expr`` to ``MultipleChoiceFilter`` (#1054) * Dropped support for EOL Python 3.4 Version 2.1 (2019-1-20) ----------------------- * Fixed a regression in ``FilterView`` introduced in 2.0. An empty ``QuerySet`` was incorrectly used whenever the FilterSet was unbound (i.e. when there were no GET parameters). The correct, pre-2.0 behaviour is now restored. A workaround was to set ``strict=False`` on the ``FilterSet``. This is no longer necessary, so you may restore `strict` behaviour as desired. * Added ``IsoDateTimeFromToRangeFilter``. Allows From-To filtering using ISO-8601 formatted dates. Version 2.0 (2018-7-13) ----------------------- 2.0 introduced a number of small changes and tidy-ups. Please see the migration guide: https://django-filter.readthedocs.io/en/main/guide/migration.html#migrating-to-2-0 * Added testing for Python 3.7 (#944) * Improve exception message for invalid filter result (#943) * Test QueryDict against CSV filters (#937) * Add `renderer` argument to `render()` method of `BooleanWidget` (#923) * Fix lookups for reverse relationships (#915) * Refactor backend filterset instantiation (#865) * Improve view-related attribute name consistency (#867) * Fix distinct call for range filters (#855) * Fix empty value check for CSV range (#854) * Rework DateRangeFilter (#852) * Added testing for Django 2.1 * Rework 'lookup types' handling into LookupChoiceFilter (#851) * Add linting and docs builds to CI (#850) * Use DRF BooleanFilter for NullBooleanField (#844) * Added Brazilian locale (#841) * List Django as a dependency in setup.py (#846) * Keep coverage reports files off version control. (#924) * Update migration docs (#866) * Added be, cs and uk translations. Updated de and ru (#861) * Slovak translation (#886) * Added Django 2.0 support. (#836) * Fix warnings build (#829) * Add greek translation (#827) * Replaced super(ClassName, self) with super() (#821) * Fixed doc URL in utils.deprecate(). (#820) * Added danish translation to django-filter (#809) * Rework validation, add queryset filter method (#788) * Fix Schema warnings (#803) * Update {Range,LookupType}Widgets to use suffixes (#770) * Method signature improvements (#800) * Remove more deprecations (#801) * Drop python 2, Django<1.11 support (#797) * Remove 'Meta.together' option (#791) * [2.x] Remove some deprecations (#795) Version 1.1 (2017-10-19) ------------------------ * Add Deprecations for 2.0 (#792) * Improve IsoDateTimeField test clarity (#790) * Fix form attr references in tests (#789) * Simplify tox config, drop python 3.3 & django 1.8 (#787) * Make get_filter_name a classmethod, allowing it to be overriden for each FilterClass (#775) * Support active timezone (#750) * Docs Typo: django_filters -> filters in docs (#773) * Add Polish translations for some messages (#771) * Remove support for Django 1.9 (EOL) (#752) * Use required attribute from field when getting schema fields (#766) * Prevent circular ImportError hiding for rest_framework sub-package (#741) * Deprecate 'extra' field attrs on Filter (#734) * Add SuffixedMultiWidget (#681) * Fix null filtering for *Choice filters (#680) * Use isort on imports (#761) * Use urlencode from django.utils.http (#760) * Remove OrderingFilter.help_text (#757) * Update DRF test dependency to 3.6 (#747) Version 1.0.4 (2017-05-19) -------------------------- Quick fix for verbose_field_name issue from 1.0.3 (#722) Version 1.0.3 (2017-05-16) -------------------------- Improves compatibility with Django REST Framework schema generation. See the `1.0.3 Milestone`__ for full details. __ https://github.com/carltongibson/django-filter/milestone/13?closed=1 Version 1.0.2 (2017-03-20) -------------------------- Updates for compatibility with Django 1.11 and Django REST Framework 3.6. Adds CI testing against Python 3.6 See the `1.0.2 Milestone`__ for full details. __ https://github.com/carltongibson/django-filter/milestone/12?closed=1 Version 1.0.1 (2016-11-28) -------------------------- Small release to ease compatibility with DRF: * #568 Adds ``rest_framework`` to the ``django_filters`` namespace to allow single ``import django_filters` usage. * A number of small updates to the docs Version 1.0 (2016-11-17) ------------------------ This release removes all the deprecated code from 0.14 and 0.15 for 1.0 #480. Please see the `Migration Notes`__ for details of how to migrate. Stick with 0.15.3 if you're not ready to update. __ https://github.com/carltongibson/django-filter/blob/1.0.0/docs/guide/migration.txt The release includes a number of small fixes and documentation updates. See the `1.0 Milestone`__ for full details. __ https://github.com/carltongibson/django-filter/milestone/8?closed=1 Version 0.15.3 (2016-10-17) --------------------------- Adds compatibility for DRF (3.5+) get_schema_fields filter backend introspection. * #492 Port get_schema_fields from DRF Version 0.15.2 (2016-09-29) --------------------------- * #507 Fix compatibility issue when not using the DTL Version 0.15.1 (2016-09-28) --------------------------- A couple of quick bug fixes: * #496 OrderingFilter not working with Select widget * #498 DRF Backend Templates not loading Version 0.15.0 (2016-09-20) --------------------------- This is a preparatory release for a 1.0. Lots of clean-up, lots of changes, mostly backwards compatible. Special thanks to Ryan P Kilby (@rpkilby) for lots of hard work. Most changes should raise a Deprecation Warning. **Note**: if you're doing *Clever Things™* with the various filter options — ``filter_overrides`` etc — you may run into an `AttributeError` since these are now defined on the metaclass and not on the filter itself. (See the discussion on #459) Summary: Highly Recommended, but take a moment to ensure everything still works. * Added the DRF backend. #481 * Deprecated `MethodFilter` in favour of `Filter.method` #382 * Move filter options to metaclass #459 * Added `get_filter_predicate` hook. (Allows e.g. filtering on annotated fields) #469 * Rework Ordering options into a filter #472 * Hardened all deprecations for 1.0. Please do see the `Migration Notes`__ __ https://github.com/carltongibson/django-filter/blob/1.0.0/docs/guide/migration.txt Version 0.14.0 (2016-08-14) --------------------------- * Confirmed support for Django 1.10. * Add support for filtering on DurationField (new in Django 1.8). * Fix UUIDFilter import issue * Improve FieldLookupError message * Add filters_for_model to improve extensibility * Fix limit_choices_to behavior with callables * Fix distinct behavior for range filters * Various Minor Clean up issues. Version 0.13.0 (2016-03-11) --------------------------- * Add support for filtering by CSV #363 * Add DateTimeFromToRangeFilter #376 * Add Chinese translation #359 * Lots of fixes. Version 0.12.0 (2016-01-07) --------------------------- * Raised minimum Django version to 1.8.x * FEATURE: Add support for custom ORM lookup types #221 * FEATURE: Add JavaScript friendly BooleanWidget #270 * FIXED: (More) Compatability with Django 1.8 and Django 1.9+ * BREAKING CHANGE: custom filter names are now also be used for ordering #230 If you use ordering on a field you defined as custom filter with custom name, you should now use the filter name as ordering key as well. Eg. For a filter like : class F(FilterSet): account = CharFilter(name='username') class Meta: model = User fields = ['account', 'status'] order_by = True Before, ordering was like `?o=username`. Since 0.12.0 it's `o=account`. Version 0.11.0 (2015-08-14) --------------------------- * FEATURE: Added default filter method lookup for MethodFilter #222 * FEATURE: Added support for yesterday in daterangefilter #234 * FEATURE: Created Filter for NumericRange. #236 * FEATURE: Added Date/time range filters #215 * FEATURE: Added option to raise with `strict` #255 * FEATURE: Added Form Field and Filter to parse ISO-8601 timestamps Version 0.10.0 (2015-05-13) --------------------- * FEATURE: Added ``conjoined`` parameter to ``MultipleChoiceFilter`` * FEATURE: Added ``together`` meta option to validate fields as a group * FIXED: Added testing on Django 1.8 * FIXED: ``get_model_field`` on Django 1.8 Version 0.9.2 (2015-01-23) -------------------------- * FIXED: Compatibility with Django v1.8a1 Version 0.9.1 (2014-12-03) -------------------------- * FIXED: Compatibility with Debug Toolbar's versions panel Version 0.9 (2014-11-28) ------------------------ * FEATURE: Allow Min/Max-Only use of RangeFilter * FEATURE: Added TypedChoiceFilter * FIXED: Correct logic for short circuit on MultipleChoiceFilter Added `always_filter` attribute and `is_noop()` test to apply short-circuiting. Set `always_filter` to `False` on init to apply default `is_noop()` test. Override `is_noop()` for more complex cases. * MISC: Version bumping with ``bumpversion`` Version 0.8 (2014-09-29) ------------------------ * FEATURE: Added exclusion filters support * FEATURE: Added `fields` dictionary shorthand syntax * FEATURE: Added `MethodFilter`. * FIXED: #115 "filters.Filter.filter() fails if it receives [] or () as value" * MISC: Various Documentation and Testing improvements Version 0.7 (2013-08-10) ------------------------ * FEATURE: Added support for AutoField. * FEATURE: There is a "distinct" flag to ensure that only unique rows are returned. * FEATURE: Support descending ordering (slighty backwards incompatible). * FEATURE: Support "strict" querysets, ie wrong filter data returns no results. * FIXED: Some translation strings were changed to be in line with admin. * FIXED: Support for Django 1.7. Version 0.6 (2013-03-25) ------------------------ * raised minimum Django version to 1.4.x * added Python 3.2 and Python 3.3 support * added Django 1.5 support and initial 1.6 compatability * FEATURE: recognition of custom model field subclasses * FEATURE: allow optional display names for order_by values * FEATURE: addition of class-based FilterView * FEATURE: addition of count() method on FilterSet to prevent pagination from loading entire queryset * FIXED: attempts to filter on reverse side of m2m, o2o or fk would raise an error Version 0.5.4 (2012-11-16) -------------------------- * project brought back to life django-filter-23.5/runtests.py0000755000175000017500000000050314533556010016314 0ustar carstencarsten#!/usr/bin/env python import os import sys from django.core.management import execute_from_command_line def runtests(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") argv = sys.argv[:1] + ["test"] + sys.argv[1:] execute_from_command_line(argv) if __name__ == "__main__": runtests() django-filter-23.5/.github/0000755000175000017500000000000014533556010015412 5ustar carstencarstendjango-filter-23.5/.github/ISSUE_TEMPLATE/0000755000175000017500000000000014533556010017575 5ustar carstencarstendjango-filter-23.5/.github/ISSUE_TEMPLATE/bug_report.md0000644000175000017500000000015514533556010022270 0ustar carstencarsten--- name: Bug report about: Found an issue, here's the place. Got a question? Use Discussions instead. --- django-filter-23.5/.github/workflows/0000755000175000017500000000000014533556010017447 5ustar carstencarstendjango-filter-23.5/.github/workflows/tests.yml0000644000175000017500000000412214533556010021333 0ustar carstencarsten--- name: Tests on: push: branches: - main pull_request: jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Ensure latest setuptools run: | python -m pip install --upgrade pip setuptools - name: Install dependencies run: | python -m pip install coverage tox tox-py unittest-xml-reporting - name: Run tox run: | python -m pip --version python -m tox --version python -m tox --py current - name: Coverage reporting run: | coverage combine coverage report -m coverage xml coverage html - name: Publish coverage results uses: codecov/codecov-action@v1 isort: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.10" - name: Ensure latest setuptools run: | python -m pip install --upgrade pip setuptools - name: Install dependencies run: | python -m pip install tox - name: Run tox run: | python -m pip --version python -m tox --version python -m tox -e isort,lint,docs warnings: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.10" - name: Ensure latest setuptools run: | python -m pip install --upgrade pip setuptools - name: Install dependencies run: | python -m pip install tox - name: Run tox run: | python -m pip --version python -m tox --version python -m tox -e warnings django-filter-23.5/LICENSE0000644000175000017500000000271714533556010015066 0ustar carstencarstenCopyright (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. django-filter-23.5/pyproject.toml0000644000175000017500000000374114533556010016773 0ustar carstencarsten[build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "django-filter" authors = [{name = "Alex Gaynor", email = "alex.gaynor@gmail.com"}] maintainers = [{name = "Carlton Gibson", email = "carlton.gibson@noumenal.es"}] license = {text = "BSD"} description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." readme = "README.rst" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] requires-python = ">=3.7" dependencies = ["Django>=3.2"] dynamic = ["version"] [project.urls] Homepage = "https://github.com/carltongibson/django-filter/tree/main" Documentation = "https://django-filter.readthedocs.io/en/main/" Changelog = "https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst" "Bug Tracker" = "https://github.com/carltongibson/django-filter/issues" "Source Code" = "https://github.com/carltongibson/django-filter" [tool.setuptools] zip-safe = false include-package-data = true license-files = ["LICENSE"] [tool.setuptools.packages.find] exclude = ["tests*"] namespaces = false [tool.isort] profile = "black" skip = [".tox"] known_third_party = ["django", "pytz", "rest_framework"] known_first_party = ["django_filters"] [tool.flit.module] name = "django_filters"