diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3473365..76317ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9] + python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"] steps: - uses: actions/checkout@v1 diff --git a/.gitignore b/.gitignore index dd56309..7190a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ tests/db.sqlite* /tests/local.db /.venv .vscode/settings.json +pyrightconfig.json diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f102cf3..c528541 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,51 @@ Changelog ========= +2.0.0 - Support Django 3.2 and 4.0 +---------------------------------- + +**BREAKING CHANGE:** This release is the first 2.x release, and drops support for EOL python and Django versions, all feature development will be done against 2.X branch.** + +Changes since 1.4.0: + +Features +~~~~~~~~ + +- Add support for python 3.10 and Django 4.0 (Merge 7bfb5b6) +- Add compiled IT translation (Merge e39395f) + +Bug fixes +~~~~~~~~~ + +- Don't add empty form to AdvancedFilterFormSet.forms (Merge 7bfb5b6) + +Other +~~~~~ + +- Drop support for EOL Python 2.7 and 3.5 (Merge dfeb005) +- Drop support for EOL Django 3.0 (Merge dfeb005) +- Drop support for EOL Django up to 2.2 (Merge dfeb005) +- Upgrade Python syntax with pyupgrade --py36-plus (Merge dfeb005) +- Remove six (Merge dfeb005) +- Remove unused import (Merge dfeb005) +- Drop support for python 3.6 (Merge 7bfb5b6) +- Correct support matrix (Merge 7bfb5b6) +- Simplify url path import (Merge 7bfb5b6) +- Remove standalone clean env from tox envlist (Merge 7bfb5b6) +- Remove unused cached_property import (Merge 7bfb5b6) +- Add Django 3.2 to classifiers (#163) +- f-string for model_name string interpolation (Merge dfeb005) +- remove unsupported django 3.1 from tox matrix (Merge 7bfb5b6) +- update README and remove Django 3.1 classifier + +Contributors +~~~~~~~~~~~~ + +- Fabrizio Corallini +- Dmytro Litvinov +- Hugo van Kemenade +- Pavel Savchenko + 1.4.0 - Latvian translation and minor fixes ------------------------------------------- diff --git a/README.rst b/README.rst index 5bc5ceb..49c32fe 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ For release notes, see `Changelog = 1.9 (Django 1.9 - 3.1 on Python 2/3/PyPy3) +- Django 2.2, >= 3.2 on Python 3.6+/PyPy3 - simplejson >= 3.6.5, < 4 diff --git a/advanced_filters/__init__.py b/advanced_filters/__init__.py index 96e3ce8..afced14 100644 --- a/advanced_filters/__init__.py +++ b/advanced_filters/__init__.py @@ -1 +1 @@ -__version__ = '1.4.0' +__version__ = '2.0.0' diff --git a/advanced_filters/admin.py b/advanced_filters/admin.py index 10b7b34..fce4831 100644 --- a/advanced_filters/admin.py +++ b/advanced_filters/admin.py @@ -5,17 +5,11 @@ from django.contrib.admin.utils import unquote from django.http import HttpResponseRedirect from django.shortcuts import resolve_url +from django.utils.translation import gettext_lazy as _ from .forms import AdvancedFilterForm from .models import AdvancedFilter -# django < 1.9 support -from django import VERSION -if VERSION >= (2, 0): - from django.utils.translation import gettext_lazy as _ -else: - from django.utils.translation import ugettext_lazy as _ - logger = logging.getLogger('advanced_filters.admin') @@ -28,16 +22,20 @@ class AdvancedListFilters(admin.SimpleListFilter): def lookups(self, request, model_admin): if not model_admin: - raise Exception('Cannot use AdvancedListFilters without a ' - 'model_admin') - model_name = "%s.%s" % (model_admin.model._meta.app_label, - model_admin.model._meta.object_name) + raise Exception( + "Cannot use AdvancedListFilters without a model_admin" + ) + model_name = ( + f"{model_admin.model._meta.app_label}." + f"{model_admin.model._meta.object_name}" + ) return AdvancedFilter.objects.filter_by_user(request.user).filter( model=model_name).values_list('id', 'title') def queryset(self, request, queryset): if self.value(): filters = AdvancedFilter.objects.filter(id=self.value()) + advfilter = None if hasattr(filters, 'first'): advfilter = filters.first() if not advfilter: @@ -49,14 +47,17 @@ def queryset(self, request, queryset): return queryset -class AdminAdvancedFiltersMixin(object): +class AdminAdvancedFiltersMixin: """ Generic AdvancedFilters mixin """ advanced_change_list_template = "admin/advanced_filters.html" advanced_filter_form = AdvancedFilterForm def __init__(self, *args, **kwargs): - super(AdminAdvancedFiltersMixin, self).__init__(*args, **kwargs) - self.original_change_list_template = "admin/change_list.html" + super().__init__(*args, **kwargs) + if self.change_list_template: + self.original_change_list_template = self.change_list_template + else: + self.original_change_list_template = "admin/change_list.html" self.change_list_template = self.advanced_change_list_template # add list filters to filters self.list_filter = (AdvancedListFilters,) + tuple(self.list_filter) @@ -103,8 +104,7 @@ def changelist_view(self, request, extra_context=None): if response: return response - return super(AdminAdvancedFiltersMixin, self - ).changelist_view(request, extra_context=extra_context) + return super().changelist_view(request, extra_context=extra_context) class AdvancedFilterAdmin(admin.ModelAdmin): @@ -123,20 +123,20 @@ def save_model(self, request, new_object, *args, **kwargs): if new_object and not new_object.pk: new_object.created_by = request.user - super(AdvancedFilterAdmin, self).save_model( + super().save_model( request, new_object, *args, **kwargs) def change_view(self, request, object_id, form_url='', extra_context=None): - orig_response = super(AdvancedFilterAdmin, self).change_view( + orig_response = super().change_view( request, object_id, form_url, extra_context) if '_save_goto' in request.POST: obj = self.get_object(request, unquote(object_id)) if obj: app, model = obj.model.split('.') - path = resolve_url('admin:%s_%s_changelist' % ( + path = resolve_url('admin:{}_{}_changelist'.format( app, model.lower())) url = "{path}{qparams}".format( - path=path, qparams="?_afilter={id}".format(id=object_id)) + path=path, qparams=f"?_afilter={object_id}") return HttpResponseRedirect(url) return orig_response @@ -147,18 +147,18 @@ def user_has_permission(user): def get_queryset(self, request): if self.user_has_permission(request.user): - return super(AdvancedFilterAdmin, self).get_queryset(request) + return super().get_queryset(request) else: return self.model.objects.filter_by_user(request.user) def has_change_permission(self, request, obj=None): if obj is None: - return super(AdvancedFilterAdmin, self).has_change_permission(request) + return super().has_change_permission(request) return self.user_has_permission(request.user) or obj in self.model.objects.filter_by_user(request.user) def has_delete_permission(self, request, obj=None): if obj is None: - return super(AdvancedFilterAdmin, self).has_delete_permission(request) + return super().has_delete_permission(request) return self.user_has_permission(request.user) or obj in self.model.objects.filter_by_user(request.user) diff --git a/advanced_filters/form_helpers.py b/advanced_filters/form_helpers.py index 681be77..8fbe474 100644 --- a/advanced_filters/form_helpers.py +++ b/advanced_filters/form_helpers.py @@ -3,8 +3,6 @@ from django import forms -import six - logger = logging.getLogger('advanced_filters.form_helpers') extra_spaces_pattern = re.compile(r'\s+') @@ -29,7 +27,7 @@ def to_python(self, value): >>> assert field.to_python('and,me') == '(and|me)' >>> assert field.to_python('and,me;too') == '(and|me;too)' """ - res = super(VaryingTypeCharField, self).to_python(value) + res = super().to_python(value) split_res = res.split(self._default_separator) if not res or len(split_res) < 2: return res.strip() @@ -40,7 +38,7 @@ def to_python(self, value): return res -class CleanWhiteSpacesMixin(object): +class CleanWhiteSpacesMixin: """ This mixin, when added to any form subclass, adds a clean method which strips repeating spaces in and around each string value of "clean_data". @@ -55,9 +53,9 @@ def clean(self): >>> assert form.is_valid() >>> assert form.cleaned_data == {'some_field': 'a weird value'} """ - cleaned_data = super(CleanWhiteSpacesMixin, self).clean() + cleaned_data = super().clean() for k in self.cleaned_data: - if isinstance(self.cleaned_data[k], six.string_types): + if isinstance(self.cleaned_data[k], str): cleaned_data[k] = re.sub(extra_spaces_pattern, ' ', self.cleaned_data[k] or '').strip() return cleaned_data diff --git a/advanced_filters/forms.py b/advanced_filters/forms.py index efb50d1..0ffe761 100644 --- a/advanced_filters/forms.py +++ b/advanced_filters/forms.py @@ -14,23 +14,14 @@ from django.db.models import Q from django.db.models.fields import DateField from django.forms.formsets import formset_factory, BaseFormSet -from django.utils.functional import cached_property -from six.moves import range, reduce +from functools import reduce from django.utils.text import capfirst +from django.utils.translation import gettext_lazy as _ from .models import AdvancedFilter from .form_helpers import CleanWhiteSpacesMixin, VaryingTypeCharField -# django < 1.9 support -from django import VERSION -if VERSION >= (2, 0): - from django.utils.translation import gettext_lazy as _ -else: - from django.utils.translation import ugettext_lazy as _ - -# django < 1.9 support -USE_VENDOR_DIR = VERSION >= (1, 9) logger = logging.getLogger('advanced_filters.forms') # select2 location can be modified via settings @@ -84,7 +75,7 @@ def _build_field_choices(self, fields): Iterate over passed model fields tuple and update initial choices. """ return tuple(sorted( - [(fquery, capfirst(fname)) for fquery, fname in fields.items()], + ((fquery, capfirst(fname)) for fquery, fname in fields.items()), key=lambda f: f[1].lower()) ) + self.FIELD_CHOICES @@ -166,7 +157,7 @@ def set_range_value(self, data): data['value'] = (dtfrom, dtto) def clean(self): - cleaned_data = super(AdvancedFilterQueryForm, self).clean() + cleaned_data = super().clean() if cleaned_data.get('operator') == "range": if ('value_from' in cleaned_data and 'value_to' in cleaned_data): @@ -184,7 +175,7 @@ def make_query(self, *args, **kwargs): return query def __init__(self, model_fields={}, *args, **kwargs): - super(AdvancedFilterQueryForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.FIELD_CHOICES = self._build_field_choices(model_fields) self.fields['field'].choices = self.FIELD_CHOICES if not self.fields['field'].initial: @@ -198,24 +189,16 @@ class AdvancedFilterFormSet(BaseFormSet): def __init__(self, *args, **kwargs): self.model_fields = kwargs.pop('model_fields', {}) - super(AdvancedFilterFormSet, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.forms: form = self.forms[0] self.fields = form.visible_fields() def get_form_kwargs(self, index): - kwargs = super(AdvancedFilterFormSet, self).get_form_kwargs(index) + kwargs = super().get_form_kwargs(index) kwargs['model_fields'] = self.model_fields return kwargs - @cached_property - def forms(self): - # override the original property to include `model_fields` argument - forms = [self._construct_form(i, model_fields=self.model_fields) - for i in range(self.total_form_count())] - forms.append(self.empty_form) # add initial empty form - return forms - AFQFormSet = formset_factory( AdvancedFilterQueryForm, formset=AdvancedFilterFormSet, @@ -234,7 +217,7 @@ class Meta: class Media: required_js = [ - 'admin/js/%sjquery.min.js' % ('vendor/jquery/' if USE_VENDOR_DIR else ''), + 'admin/js/vendor/jquery/jquery.min.js', 'advanced-filters/jquery_adder.js', 'orig_inlines%s.js' % ('' if settings.DEBUG else '.min'), 'magnific-popup/jquery.magnific-popup.js', @@ -294,7 +277,7 @@ def __init__(self, *args, **kwargs): self._filter_fields = filter_fields or getattr( model_admin, 'advanced_filter_fields', ()) - super(AdvancedFilterForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # populate existing or empty forms formset data = None @@ -305,15 +288,15 @@ def __init__(self, *args, **kwargs): self.initialize_form(instance, self._model, data, extra_form) def clean(self): - cleaned_data = super(AdvancedFilterForm, self).clean() + cleaned_data = super().clean() if not self.fields_formset.is_valid(): logger.debug( "Errors validating advanced query filters: %s", pformat([(f.errors, f.non_field_errors()) for f in self.fields_formset.forms])) raise forms.ValidationError("Error validating filter forms") - cleaned_data['model'] = "%s.%s" % (self._model._meta.app_label, - self._model._meta.object_name) + cleaned_data['model'] = "{}.{}".format(self._model._meta.app_label, + self._model._meta.object_name) return cleaned_data @property @@ -364,4 +347,4 @@ def initialize_form(self, instance, model, data=None, extra=None): def save(self, commit=True): self.instance.query = self.generate_query() self.instance.model = self.cleaned_data.get('model') - return super(AdvancedFilterForm, self).save(commit) + return super().save(commit) diff --git a/advanced_filters/locale/it/LC_MESSAGES/django.mo b/advanced_filters/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000..b7340cf Binary files /dev/null and b/advanced_filters/locale/it/LC_MESSAGES/django.mo differ diff --git a/advanced_filters/locale/it/LC_MESSAGES/django.po b/advanced_filters/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000..910d6ca --- /dev/null +++ b/advanced_filters/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,146 @@ +# 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: 2018-01-25 09:49+0100\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=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" + +#: admin.py:24 +msgid "Advanced filters" +msgstr "Filtri avanzati" + +#: admin.py:81 +msgid "Advanced filter added successfully." +msgstr "Filtro aggiunto correttamente." + +#: forms.py:58 +msgid "Equals" +msgstr "Uguale" + +#: forms.py:59 +msgid "Contains" +msgstr "Contiene" + +#: forms.py:60 +msgid "One of" +msgstr "Uno di" + +#: forms.py:61 +msgid "DateTime Range" +msgstr "Intervallo data e ora" + +#: forms.py:62 +msgid "Is NULL" +msgstr "è Nullo" + +#: forms.py:63 +msgid "Is TRUE" +msgstr "è Vero" + +#: forms.py:64 +msgid "Is FALSE" +msgstr "è Falso" + +#: forms.py:65 +msgid "Less Than" +msgstr "Minore di" + +#: forms.py:66 +msgid "Greater Than" +msgstr "Maggiore di" + +#: forms.py:67 +msgid "Less Than or Equal To" +msgstr "Minore o uguale a" + +#: forms.py:68 +msgid "Greater Than or Equal To" +msgstr "Maggiore o uguale a" + +#: forms.py:72 +msgid "Or (mark an or between blocks)" +msgstr "O (inserisci un or tra i blocchi)" + +#: forms.py:76 +msgid "Field" +msgstr "Campo" + +#: forms.py:79 +msgid "Operator" +msgstr "Operatore" + +#: forms.py:81 +msgid "Value" +msgstr "Valore" + +#: forms.py:83 +msgid "Value from" +msgstr "Valore da" + +#: forms.py:85 +msgid "Value to" +msgstr "Valore a" + +#: forms.py:86 +msgid "Negate" +msgstr "Negazione" + +#: models.py:18 templates/admin/advanced_filters.html:16 +msgid "Advanced Filter" +msgstr "Filtro avanzato" + +#: models.py:19 +msgid "Advanced Filters" +msgstr "Filtri avanzati" + +#: models.py:22 +msgid "Title" +msgstr "Titolo" + +#: templates/admin/advanced_filters.html:16 +msgid "Edit" +msgstr "Modifica" + +#: templates/admin/advanced_filters.html:28 +msgid "Create advanced filter" +msgstr "Crea filtro avanzato" + +#: templates/admin/advanced_filters.html:66 +msgid "Save" +msgstr "Salva" + +#: templates/admin/advanced_filters.html:67 +#: templates/admin/advanced_filters/change_form.html:52 +msgid "Save & Filter Now!" +msgstr "Salva e filtra!" + +#: templates/admin/advanced_filters.html:68 +msgid "Cancel" +msgstr "Cancella" + +#: templates/admin/advanced_filters/change_form.html:16 +msgid "Change advanced filter" +msgstr "Cambia filtro avanzato" + +#: templates/admin/common_js_init.html:14 +msgid "Add another filter" +msgstr "Aggiungi un altro filtro" + +#: templates/admin/common_js_init.html:15 +msgid "Remove" +msgstr "Elimina" \ No newline at end of file diff --git a/advanced_filters/migrations/0001_initial.py b/advanced_filters/migrations/0001_initial.py index 2fd49d9..dd25c59 100644 --- a/advanced_filters/migrations/0001_initial.py +++ b/advanced_filters/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9.4 on 2016-03-07 23:02 -from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models diff --git a/advanced_filters/migrations/0002_advancedfilter_created_at.py b/advanced_filters/migrations/0002_advancedfilter_created_at.py index 092e2b9..8d1cfea 100644 --- a/advanced_filters/migrations/0002_advancedfilter_created_at.py +++ b/advanced_filters/migrations/0002_advancedfilter_created_at.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/advanced_filters/mixins.py b/advanced_filters/mixins.py index 12b69a8..3a5be15 100644 --- a/advanced_filters/mixins.py +++ b/advanced_filters/mixins.py @@ -16,13 +16,8 @@ from django.utils.encoding import force_text as force_string from django.views.decorators.csrf import csrf_exempt -try: - from django.utils import six -except ImportError: - import six - -class CsrfExemptMixin(object): +class CsrfExemptMixin: """ Exempts the view from CSRF requirements. NOTE: @@ -31,10 +26,10 @@ class CsrfExemptMixin(object): @method_decorator(csrf_exempt) def dispatch(self, *args, **kwargs): - return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) + return super().dispatch(*args, **kwargs) -class AccessMixin(object): +class AccessMixin: """ 'Abstract' mixin that gives access mixins the same customizable functionality. @@ -104,11 +99,11 @@ def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: return self.handle_no_permission(request) - return super(StaffuserRequiredMixin, self).dispatch( + return super().dispatch( request, *args, **kwargs) -class JSONResponseMixin(object): +class JSONResponseMixin: """ A mixin that allows you to easily serialize simple data such as a dict or Django models. @@ -120,7 +115,7 @@ class JSONResponseMixin(object): def get_content_type(self): if (self.content_type is not None and not isinstance(self.content_type, - (six.string_types, six.text_type))): + ((str,), str))): raise ImproperlyConfigured( '{0} is missing a content type. Define {0}.content_type, ' 'or override {0}.get_content_type().'.format( diff --git a/advanced_filters/models.py b/advanced_filters/models.py index 33f1b28..0e4f835 100644 --- a/advanced_filters/models.py +++ b/advanced_filters/models.py @@ -1,16 +1,10 @@ from django.conf import settings from django.db import models from django.db.models import Q +from django.utils.translation import gettext_lazy as _ from .q_serializer import QSerializer -# django < 1.9 support -from django import VERSION -if VERSION >= (2, 0): - from django.utils.translation import gettext_lazy as _ -else: - from django.utils.translation import ugettext_lazy as _ - class UserLookupManager(models.Manager): def filter_by_user(self, user): diff --git a/advanced_filters/q_serializer.py b/advanced_filters/q_serializer.py index c0bc668..322cfcc 100644 --- a/advanced_filters/q_serializer.py +++ b/advanced_filters/q_serializer.py @@ -3,7 +3,6 @@ import base64 import time -import six from django.db.models import Q from django.core.serializers.base import SerializationError @@ -25,7 +24,7 @@ def dt2ts(obj): return time.mktime(obj.timetuple()) if isinstance(obj, date) else obj -class QSerializer(object): +class QSerializer: """ A Q object serializer base class. Pass base64=True when initializing to Base-64 encode/decode the returned/passed string. @@ -121,7 +120,7 @@ def dumps(self, obj): raise SerializationError string = json.dumps(self.serialize(obj), default=dt2ts) if self.b64_enabled: - return base64.b64encode(six.b(string)).decode("utf-8") + return base64.b64encode(string.encode("latin-1")).decode("utf-8") return string def loads(self, string, raw=False): diff --git a/advanced_filters/tests/test_admin_change_form.py b/advanced_filters/tests/test_admin_change_form.py index 540376e..fd4c248 100644 --- a/advanced_filters/tests/test_admin_change_form.py +++ b/advanced_filters/tests/test_admin_change_form.py @@ -1,15 +1,11 @@ import pytest from django.contrib.auth.models import Permission from django.db.models import Q +from django.urls import reverse from ..models import AdvancedFilter from .factories import AdvancedFilterFactory -try: - from django.urls import reverse -except ImportError: # Django < 2.0 - from django.core.urlresolvers import reverse - URL_NAME_CHANGE = "admin:advanced_filters_advancedfilter_change" URL_NAME_ADD = "admin:advanced_filters_advancedfilter_add" URL_NAME_CLIENT_CHANGELIST = "admin:customers_client_changelist" diff --git a/advanced_filters/tests/test_creation.py b/advanced_filters/tests/test_creation.py index de3cab0..af1805c 100644 --- a/advanced_filters/tests/test_creation.py +++ b/advanced_filters/tests/test_creation.py @@ -1,13 +1,9 @@ import pytest from django.contrib.auth.models import Permission +from django.urls import reverse_lazy from ..models import AdvancedFilter -try: - from django.urls import reverse_lazy -except ImportError: # Django < 2.0 - from django.core.urlresolvers import reverse_lazy - URL_CLIENT_CHANGELIST = reverse_lazy("admin:customers_client_changelist") @@ -83,6 +79,6 @@ def test_create_form_valid(user, client, good_data, query): created_filter = AdvancedFilter.objects.order_by("pk").last() url = res["location"] - assert url.endswith("%s?_afilter=%s" % (URL_CLIENT_CHANGELIST, created_filter.pk)) + assert url.endswith(f"{URL_CLIENT_CHANGELIST}?_afilter={created_filter.pk}") assert list(created_filter.query.children[0]) == query diff --git a/advanced_filters/tests/test_forms.py b/advanced_filters/tests/test_forms.py index 7cb2e7f..4445e76 100644 --- a/advanced_filters/tests/test_forms.py +++ b/advanced_filters/tests/test_forms.py @@ -106,17 +106,8 @@ def test_make_query(self): assert isinstance(q, Q) assert isinstance(q.children, list) assert q.connector == 'AND' - if django.VERSION >= (2, 0): - # django 2+ flattens nested empty Query - assert q.negated - assert q.children[0] == ('fname__iexact', 'john') - else: - # django <2 has a parent Query that stays default - assert not q.negated - subquery = q.children[0] - assert isinstance(subquery, Q) - assert subquery.negated - assert subquery.children[0] == ('fname__iexact', 'john') + assert q.negated + assert q.children[0] == ('fname__iexact', 'john') def test_invalid_existing_query(self): Rep = get_user_model() @@ -228,8 +219,8 @@ def setUp(self): created_by=self.user) def _create_query_form_data(self, form_number=0, data=None, **kwargs): - form_data = dict(('form-%d-%s' % (form_number, k), v) - for k, v in (data or self.formset_data).items()) + form_data = {'form-%d-%s' % (form_number, k): v + for k, v in (data or self.formset_data).items()} form_data.update(self.mgmg_form_data) form_data.update(dict(title='baz filter')) form_data.update(kwargs) @@ -245,9 +236,9 @@ def test_failed_validation(self): **self.default_error) assert form.non_field_errors() == self.default_non_field_err assert form.fields_formset.errors == [ - {'operator': [u'This field is required.'], - 'field': [u'This field is required.'], - 'value': [u'This field is required.']}] + {'operator': ['This field is required.'], + 'field': ['This field is required.'], + 'value': ['This field is required.']}] def test_invalid_field_validation(self): form = AdvancedFilterForm(self._create_query_form_data(), instance=self.af, @@ -288,7 +279,7 @@ def test_remove_existing_query(self): class TestAdminInitialization(CommonFormTest): def setUp(self): - super(TestAdminInitialization, self).setUp() + super().setUp() self.fdata = self._create_query_form_data(form_number=0, data={ 'field': 'groups__name', 'negate': False, 'operator': 'iexact', 'value': 'bar'}) @@ -332,7 +323,7 @@ def test_field_resolution(self): def test_create_instance_with_modeladmin(self): form = AdvancedFilterForm(data=self.fdata, model_admin=self.rep_model_admin) - assert form.is_valid(), 'errors: %s, %s' % (form.errors, form.fields_formset.errors) + assert form.is_valid(), f'errors: {form.errors}, {form.fields_formset.errors}' instance = form.save(commit=False) instance.created_by = self.user assert isinstance(instance, AdvancedFilter) diff --git a/advanced_filters/tests/test_get_field_choices_view.py b/advanced_filters/tests/test_get_field_choices_view.py index 28947b3..d4aa954 100644 --- a/advanced_filters/tests/test_get_field_choices_view.py +++ b/advanced_filters/tests/test_get_field_choices_view.py @@ -8,13 +8,9 @@ import pytest from django.utils import timezone from django.utils.encoding import force_str +from django.urls import reverse from tests.factories import ClientFactory -try: - from django.urls import reverse -except ImportError: # Django < 2.0 - from django.core.urlresolvers import reverse - URL_NAME = "afilters_get_field_choices" @@ -39,22 +35,9 @@ def assert_view_error(client, error, exception=None, **view_kwargs): NO_APP_INSTALLED_ERROR = "No installed app with label 'foo'." - -if django.VERSION < (1, 11): - NO_MODEL_ERROR = "App 'reps' doesn't have a 'foo' model." -else: - NO_MODEL_ERROR = "App 'reps' doesn't have a 'Foo' model." - - -if sys.version_info >= (3, 5): - ARGUMENT_LENGTH_ERROR = "not enough values to unpack (expected 2, got 1)" -else: - ARGUMENT_LENGTH_ERROR = "need more than 1 value to unpack" - -if sys.version_info < (3,) and django.VERSION < (1, 11): - MISSING_FIELD_ERROR = "SalesRep has no field named u'baz'" -else: - MISSING_FIELD_ERROR = "SalesRep has no field named 'baz'" +ARGUMENT_LENGTH_ERROR = "not enough values to unpack (expected 2, got 1)" +MISSING_FIELD_ERROR = "SalesRep has no field named 'baz'" +NO_MODEL_ERROR = "App 'reps' doesn't have a 'Foo' model." def test_invalid_view_kwargs(client): @@ -157,7 +140,7 @@ def test_choices_no_date_fields_support(user, client, settings): def test_choices_has_null(user, client, settings): settings.ADVANCED_FILTERS_MAX_CHOICES = 4 named_users = ClientFactory.create_batch(2, assigned_to=user) - names = [None] + sorted(set([nu.first_name for nu in named_users])) + names = [None] + sorted({nu.first_name for nu in named_users}) assert len(named_users) == 2 ClientFactory.create_batch(2, assigned_to=user, first_name=None) view_url = reverse( diff --git a/advanced_filters/tests/test_q_serializer.py b/advanced_filters/tests/test_q_serializer.py index 41bae32..9912a31 100644 --- a/advanced_filters/tests/test_q_serializer.py +++ b/advanced_filters/tests/test_q_serializer.py @@ -8,7 +8,7 @@ class QSerializerTest(TestCase): correct_query = { 'children': [('test', 1234)], - 'connector': u'AND', + 'connector': 'AND', 'negated': False, } @@ -28,7 +28,7 @@ def test_jsondump_q(self): def test_deserialize_q(self): qres = self.s.deserialize({ 'children': [('test', 1234)], - 'connector': u'AND', + 'connector': 'AND', 'negated': False, 'subtree_parents': [] }) diff --git a/advanced_filters/tests/test_usage.py b/advanced_filters/tests/test_usage.py index 0155093..edca0d8 100644 --- a/advanced_filters/tests/test_usage.py +++ b/advanced_filters/tests/test_usage.py @@ -1,17 +1,13 @@ import pytest from django.contrib.auth.models import Permission from django.db.models import Q +from django.urls import reverse from tests.factories import ClientFactory, SalesRepFactory from ..admin import AdvancedListFilters from ..models import AdvancedFilter from .factories import AdvancedFilterFactory -try: - from django.urls import reverse -except ImportError: # Django < 2.0 - from django.core.urlresolvers import reverse - URL_NAME_CLIENT_CHANGELIST = "admin:customers_client_changelist" diff --git a/advanced_filters/urls.py b/advanced_filters/urls.py index a8fd76e..4fc8320 100644 --- a/advanced_filters/urls.py +++ b/advanced_filters/urls.py @@ -1,14 +1,14 @@ -from django.conf.urls import url +from django.urls import path from advanced_filters.views import GetFieldChoices urlpatterns = [ - url(r'^field_choices/(?P.+)/(?P.+)/?', + path('field_choices///', GetFieldChoices.as_view(), name='afilters_get_field_choices'), # only to allow building dynamically - url(r'^field_choices/$', + path('field_choices/', GetFieldChoices.as_view(), name='afilters_get_field_choices'), ] diff --git a/setup.py b/setup.py index 2ddca9c..761d3a5 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -import io import os import sys @@ -35,11 +34,11 @@ def get_full_description(): readme = 'README.rst' changelog = 'CHANGELOG.rst' base = os.path.dirname(__file__) - with io.open(os.path.join(base, readme), encoding='utf-8') as readme: + with open(os.path.join(base, readme), encoding='utf-8') as readme: README = readme.read() - with io.open(os.path.join(base, changelog), encoding='utf-8') as changelog: + with open(os.path.join(base, changelog), encoding='utf-8') as changelog: CHANGELOG = changelog.read() - return '%s\n%s' % (README, CHANGELOG) + return f'{README}\n{CHANGELOG}' # allow setup.py to be run from any path @@ -70,6 +69,7 @@ def get_full_description(): zip_safe=False, author='Pavel Savchenko', author_email='pavel@modlinltd.com', + python_requires='>=3.6', classifiers=[ 'Environment :: Web Environment', 'Framework :: Django', @@ -77,23 +77,16 @@ def get_full_description(): 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3 :: Only', 'Framework :: Django', - 'Framework :: Django :: 1.9', - 'Framework :: Django :: 1.10', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', + 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.0', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], diff --git a/tests/customers/migrations/0001_initial.py b/tests/customers/migrations/0001_initial.py index e6f6867..de6f1c5 100644 --- a/tests/customers/migrations/0001_initial.py +++ b/tests/customers/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9.4 on 2016-03-13 22:23 -from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models diff --git a/tests/customers/models.py b/tests/customers/models.py index f4bf75f..31238aa 100644 --- a/tests/customers/models.py +++ b/tests/customers/models.py @@ -1,13 +1,7 @@ from django.contrib.auth.models import AbstractBaseUser from django.db import models from django.utils import timezone - -# django < 1.9 support -from django import VERSION -if VERSION >= (2, 0): - from django.utils.translation import gettext_lazy as _ -else: - from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class Client(AbstractBaseUser): diff --git a/tests/factories.py b/tests/factories.py index 8fb039d..d5b2eb3 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -15,7 +15,7 @@ class Meta: @classmethod def _prepare(cls, create, **kwargs): password = kwargs.pop('password', None) - user = super(SalesRepFactory, cls)._prepare(create, **kwargs) + user = super()._prepare(create, **kwargs) if password: user.set_password(password) if create: diff --git a/tests/reps/migrations/0001_initial.py b/tests/reps/migrations/0001_initial.py index f6a2407..099a1e5 100644 --- a/tests/reps/migrations/0001_initial.py +++ b/tests/reps/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9.4 on 2016-03-13 22:23 -from __future__ import unicode_literals import django.contrib.auth.models import django.core.validators diff --git a/tests/test_project/urls.py b/tests/test_project/urls.py index f3c5556..e249e36 100644 --- a/tests/test_project/urls.py +++ b/tests/test_project/urls.py @@ -1,7 +1,7 @@ -from django.conf.urls import include, url +from django.urls import include, path from django.contrib import admin urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^advanced_filters/', include('advanced_filters.urls')) + path('admin/', admin.site.urls), + path('advanced_filters/', include('advanced_filters.urls')) ] diff --git a/tox.ini b/tox.ini index c6bb41f..25371e0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,36 +1,20 @@ [tox] envlist = - py27-django{19,110,111} - py35-django{19,110,111,20,21,22} - py36-django{111,20,21,22,30,31} - py37-django{111,20,21,22,30,31} - py38-django{22,30,31} - py39-django{22,30,31} - ; py36-django{111,20,21,22,30,31,32} - ; py37-django{111,20,21,22,30,31,32} - ; py38-django{22,30,31,32} - ; py39-django{22,30,31,32} - pypy3-django{111,20,21,22,30,31} + py{37,py37}-django{22,32} + py{38,py38,39}-django{22,32,40} + py310-django{32,40} report - clean [pycodestyle] max-line-length = 120 [testenv] usedevelop = true -passenv = TRAVIS TRAVIS_* deps = -rtest-reqs.txt - django19: Django>=1.9,<1.10 - django110: Django>=1.10,<1.11 - django111: Django>=1.11,<1.12 - django20: Django>=2.0,<2.1 - django21: Django>=2.1,<2.2 django22: Django>=2.2,<3.0 - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 - ; django32: Django>=3.2,<3.3 + django32: Django>=3.2,<3.3 + django40: Django>=4.0,<4.1 depends = !report: clean @@ -42,48 +26,20 @@ commands = pytest --cov-append . pycodestyle --exclude=urls.py,migrations,.ropeproject -v advanced_filters -[travis] -python = - 2.7: py27 - 3.5: py35 - 3.6: py36 - 3.7: py37 - 3.9: py39 - pypy3: pypy3 - -[travis:env] -DJANGO = - 1.9: django19 - 1.10: django110 - 1.11: django111 - 2.0: django20 - 2.1: django21 - 2.2: django22 - 3.0: django30 - 3.1: django31 - ; 3.2: django32 - [gh-actions] python = - 2.7: py27 - 3.5: py35 - 3.6: py36 3.7: py37 3.8: py38 3.9: py39 - pypy3: pypy3 + 3.10: py310 + pypy-3.7: pypy37 + pypy-3.8: pypy38 [gh-actions:env] DJANGO = - 1.9: django19 - 1.10: django110 - 1.11: django111 - 2.0: django20 - 2.1: django21 2.2: django22 - 3.0: django30 - 3.1: django31 - ; 3.2: django32 + 3.2: django32 + 4.0: django40 [testenv:report] deps = coverage