diff --git a/.travis.yml b/.travis.yml index 0bfac07..68bf21c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,34 +4,23 @@ cache: directories: - "~/.cache/pip" python: -- '2.7' +- '3.8' +- '3.7' - '3.6' - '3.5' env: -- DJANGO=2.0 -- DJANGO=2.1 -- DJANGO=1.11 +- DJANGO=2.2 +- DJANGO=3.0 +- DJANGO=3.1 - DJANGO=master -matrix: +jobs: exclude: - - python: '2.7' - env: DJANGO=master - - python: '2.7' - env: DJANGO=2.0 - - python: '2.7' - env: DJANGO=2.1 - - python: '3.6' - env: DJANGO=1.11 - allow_failures: + - python: '3.5' + env: DJANGO=3.0 + - python: '3.5' + env: DJANGO=3.1 - python: '3.5' env: DJANGO=master - - python: '3.6' - env: DJANGO=master - - python: '3.5' - env: DJANGO=2.1 - - python: '3.6' - env: DJANGO=2.1 - install: - pip install tox script: @@ -46,5 +35,5 @@ deploy: tags: true repo: jazzband/django-admin2 # only do the PyPI release for exactly one scenario of the test matrix: - condition: "$DJANGO = 1.11" + condition: "$DJANGO = 2.2" python: 3.6 diff --git a/djadmin2/__init__.py b/djadmin2/__init__.py index 76f005e..f2e16b2 100644 --- a/djadmin2/__init__.py +++ b/djadmin2/__init__.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - - __version__ = '0.7.1' __author__ = 'Daniel Greenfeld & Contributors' diff --git a/djadmin2/actions.py b/djadmin2/actions.py index 9638726..b78a8c3 100644 --- a/djadmin2/actions.py +++ b/djadmin2/actions.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.contrib import messages from django.db import router -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.text import capfirst from django.utils.translation import ugettext as _ -from django.utils.translation import ugettext_lazy, ungettext, pgettext_lazy +from django.utils.translation import gettext_lazy, ngettext, pgettext_lazy from django.views.generic import TemplateView from . import permissions, utils @@ -26,7 +23,7 @@ class BaseListAction(Admin2ModelMixin, TemplateView): permission_classes = (permissions.IsStaffPermission,) - empty_message = ugettext_lazy( + empty_message = gettext_lazy( 'Items must be selected in order to perform actions ' 'on them. No items have been changed.' ) @@ -50,7 +47,7 @@ class BaseListAction(Admin2ModelMixin, TemplateView): objects_name = options.verbose_name else: objects_name = options.verbose_name_plural - self.objects_name = force_text(objects_name) + self.objects_name = force_str(objects_name) super(BaseListAction, self).__init__(*args, **kwargs) @@ -92,8 +89,8 @@ class BaseListAction(Admin2ModelMixin, TemplateView): def _format_callback(obj): opts = utils.model_options(obj) - return '%s: %s' % (force_text(capfirst(opts.verbose_name)), - force_text(obj)) + return '%s: %s' % (force_str(capfirst(opts.verbose_name)), + force_str(obj)) using = router.db_for_write(self.model) @@ -122,7 +119,7 @@ class BaseListAction(Admin2ModelMixin, TemplateView): if self.process_queryset() is None: # objects_name should already be pluralized, see __init__ - message = ungettext( + message = ngettext( self.success_message, self.success_message_plural, self.item_count @@ -146,7 +143,7 @@ class DeleteSelectedAction(BaseListAction): default_template_name = "actions/delete_selected_confirmation.html" - description = ugettext_lazy("Delete selected items") + description = gettext_lazy("Delete selected items") success_message = pgettext_lazy( 'singular form', diff --git a/djadmin2/admin2.py b/djadmin2/admin2.py index abbba0a..90f7633 100644 --- a/djadmin2/admin2.py +++ b/djadmin2/admin2.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.conf import settings from django.contrib.auth.models import Group, User from django.contrib.sites.models import Site diff --git a/djadmin2/apiviews.py b/djadmin2/apiviews.py index 1a11059..513e880 100644 --- a/djadmin2/apiviews.py +++ b/djadmin2/apiviews.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.utils.encoding import force_str from rest_framework import fields, generics, serializers from rest_framework.response import Response diff --git a/djadmin2/apps.py b/djadmin2/apps.py index c991a04..eb42c75 100644 --- a/djadmin2/apps.py +++ b/djadmin2/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig from django.db.models.signals import post_migrate -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from djadmin2.permissions import create_view_permissions diff --git a/djadmin2/core.py b/djadmin2/core.py index bf04d9d..790d581 100644 --- a/djadmin2/core.py +++ b/djadmin2/core.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*-: """ WARNING: This file about to undergo major refactoring by @pydanny per Issue #99. """ -from __future__ import division, absolute_import, unicode_literals - from importlib import import_module from django.conf import settings diff --git a/djadmin2/filters.py b/djadmin2/filters.py index 6eeb58a..54b87bd 100644 --- a/djadmin2/filters.py +++ b/djadmin2/filters.py @@ -1,18 +1,14 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - -import collections +import collections.abc from itertools import chain import django_filters from django import forms from django.forms import widgets as django_widgets from django.forms.utils import flatatt -from django.utils import six -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.html import format_html from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy +from django.utils.translation import gettext_lazy from .utils import type_str @@ -33,7 +29,7 @@ class ChoicesAsLinksWidget(django_widgets.Select): for choice_value, choice_label in chain(self.choices, choices): links.append(format_html( LINK_TEMPLATE, - name, choice_value, flatatt(attrs), force_text(choice_label), + name, choice_value, flatatt(attrs), force_str(choice_label), )) return mark_safe(u"
".join(links)) @@ -45,9 +41,9 @@ class NullBooleanLinksWidget( def __init__(self, attrs=None, choices=()): super(ChoicesAsLinksWidget, self).__init__(attrs) self.choices = [ - ('1', ugettext_lazy('Unknown')), - ('2', ugettext_lazy('Yes')), - ('3', ugettext_lazy('No')), + ('1', gettext_lazy('Unknown')), + ('2', gettext_lazy('Yes')), + ('3', gettext_lazy('No')), ] @@ -70,7 +66,7 @@ def build_list_filter(request, model_admin, queryset): `request.GET` and `queryset`. """ # if ``list_filter`` is not iterable return it right away - if not isinstance(model_admin.list_filter, collections.Iterable): + if not isinstance(model_admin.list_filter, collections.abc.Iterable): return model_admin.list_filter( request.GET, queryset=queryset, @@ -78,7 +74,7 @@ def build_list_filter(request, model_admin, queryset): # otherwise build :mod:`django_filters.FilterSet` filters = [] for field_filter in model_admin.list_filter: - if isinstance(field_filter, six.string_types): + if isinstance(field_filter, str): filters.append(get_filter_for_field_name( queryset.model, field_filter, @@ -135,8 +131,9 @@ def get_filter_for_field_name(model, field_name): django_filters.filterset.get_model_field(model, field_name,), field_name, ) + print("EXTRA!!!!") + print(filter_.extra) filter_.widget = FILTER_TYPE_TO_WIDGET.get( - filter_.__class__, - filter_.widget, + filter_.__class__ ) return filter_ diff --git a/djadmin2/forms.py b/djadmin2/forms.py index d8279f7..a1b9f66 100644 --- a/djadmin2/forms.py +++ b/djadmin2/forms.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django import forms from django.contrib.auth import authenticate from django.contrib.auth.forms import AuthenticationForm @@ -8,7 +5,7 @@ from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.core.exceptions import ValidationError from django.urls import reverse_lazy -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ # Translators : %(username)s will be replaced by the username_field name diff --git a/djadmin2/migrations/0001_initial.py b/djadmin2/migrations/0001_initial.py index f3030bc..b94961b 100644 --- a/djadmin2/migrations/0001_initial.py +++ b/djadmin2/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models from django.conf import settings diff --git a/djadmin2/models.py b/djadmin2/models.py index ba10270..b48d7d3 100644 --- a/djadmin2/models.py +++ b/djadmin2/models.py @@ -1,14 +1,10 @@ -# -*- coding: utf-8 -*- """ Boilerplate for now, will serve a purpose soon! """ -from __future__ import division, absolute_import, unicode_literals - from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db import models -from django.utils.encoding import force_text -from django.utils.encoding import python_2_unicode_compatible +from django.utils.encoding import force_str from django.utils.encoding import smart_text -from django.utils.translation import ugettext, ugettext_lazy as _ +from django.utils.translation import ugettext, gettext_lazy as _ from .utils import quote @@ -17,12 +13,11 @@ class LogEntryManager(models.Manager): def log_action(self, user_id, obj, action_flag, change_message=''): content_type_id = ContentType.objects.get_for_model(obj).id e = self.model(None, None, user_id, content_type_id, - smart_text(obj.id), force_text(obj)[:200], + smart_text(obj.id), force_str(obj)[:200], action_flag, change_message) e.save() -@python_2_unicode_compatible class LogEntry(models.Model): ADDITION = 1 CHANGE = 2 diff --git a/djadmin2/permissions.py b/djadmin2/permissions.py index 200fc3f..fbeb00b 100644 --- a/djadmin2/permissions.py +++ b/djadmin2/permissions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ djadmin2's permission handling. The permission classes have the same API as the permission handling classes of the django-rest-framework. That way, we can @@ -15,8 +14,6 @@ interface: The permission classes are then just fancy wrappers of these basic checks of which it can hold multiple. """ -from __future__ import division, absolute_import, unicode_literals - import logging import re @@ -25,8 +22,7 @@ from django.db.utils import DEFAULT_DB_ALIAS from django.apps import apps from django.core.exceptions import ValidationError from django.db import router -from django.utils import six -from django.utils.encoding import python_2_unicode_compatible, force_text +from django.utils.encoding import force_str logger = logging.getLogger('djadmin2') @@ -191,7 +187,6 @@ class ModelDeletePermission(BasePermission): permissions = (model_permission('{app_label}.delete_{model_name}'),) -@python_2_unicode_compatible class TemplatePermissionChecker(object): ''' Can be used in the template like: @@ -286,7 +281,7 @@ class TemplatePermissionChecker(object): Return a clone of the permission wrapper with a new model_admin bind to it. ''' - if isinstance(admin, six.string_types): + if isinstance(admin, str): try: admin = self._model_admin.admin.get_admin_by_name(admin) except ValueError: @@ -300,7 +295,7 @@ class TemplatePermissionChecker(object): ''' Return a clone of the permission wrapper with a new view bind to it. ''' - if isinstance(view, six.string_types): + if isinstance(view, str): if view not in self.view_name_mapping: return '' view_name = self.view_name_mapping[view] @@ -365,7 +360,7 @@ class TemplatePermissionChecker(object): def __str__(self): if self._view is None: return '' - return force_text(bool(self)) + return force_str(bool(self)) def create_view_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): # noqa diff --git a/djadmin2/renderers.py b/djadmin2/renderers.py index 614528f..52e8547 100644 --- a/djadmin2/renderers.py +++ b/djadmin2/renderers.py @@ -1,17 +1,14 @@ -# -*- coding: utf-8 -*- """ There are currently a few renderers that come directly with django-admin2. They are used by default for some field types. """ -from __future__ import division, absolute_import, unicode_literals - import os.path from datetime import date, time, datetime from django.db import models from django.template.loader import render_to_string from django.utils import formats, timezone -from django.utils.encoding import force_text +from django.utils.encoding import force_str from djadmin2 import settings @@ -65,7 +62,7 @@ def title_renderer(value, field): :rtype: unicode or str """ - return force_text(value).title() + return force_str(value).title() def number_renderer(value, field): diff --git a/djadmin2/settings.py b/djadmin2/settings.py index 0d90930..5960443 100644 --- a/djadmin2/settings.py +++ b/djadmin2/settings.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.conf import settings diff --git a/djadmin2/templatetags/admin2_tags.py b/djadmin2/templatetags/admin2_tags.py index 7625ed7..0595cb1 100644 --- a/djadmin2/templatetags/admin2_tags.py +++ b/djadmin2/templatetags/admin2_tags.py @@ -1,11 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from numbers import Number from datetime import date, time, datetime from django import template -from django.db.models.fields import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist from .. import utils, renderers, models, settings diff --git a/djadmin2/tests/migrations/0001_initial.py b/djadmin2/tests/migrations/0001_initial.py index 700cd16..90601fd 100644 --- a/djadmin2/tests/migrations/0001_initial.py +++ b/djadmin2/tests/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/djadmin2/tests/test_renderers.py b/djadmin2/tests/test_renderers.py index 3e53b20..163e052 100644 --- a/djadmin2/tests/test_renderers.py +++ b/djadmin2/tests/test_renderers.py @@ -1,11 +1,7 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - import datetime as dt from decimal import Decimal from django.test import TestCase -from django.utils import six from django.utils.translation import activate from .. import renderers @@ -106,10 +102,7 @@ class NumberRendererTest(TestCase): def testEndlessFloat(self): out = self.renderer(1.0 / 3, None) - if six.PY2: - self.assertEqual('0.333333333333', out) - else: - self.assertEqual('0.3333333333333333', out) + self.assertEqual('0.3333333333333333', out) def testPlainDecimal(self): number = '0.123456789123456789123456789' diff --git a/djadmin2/tests/test_utils.py b/djadmin2/tests/test_utils.py index d54f43b..1264b82 100644 --- a/djadmin2/tests/test_utils.py +++ b/djadmin2/tests/test_utils.py @@ -1,5 +1,4 @@ from django.test import TestCase -from django.utils import six from .. import utils from ..views import IndexView @@ -137,16 +136,10 @@ class UtilsTest(TestCase): def __unicode__(self): return "unicode" - if six.PY2: - self.assertEqual( - utils.get_attr(Klass(), "__str__"), - "unicode" - ) - else: - self.assertEqual( - utils.get_attr(Klass(), "__str__"), - "str" - ) + self.assertEqual( + utils.get_attr(Klass(), "__str__"), + "str" + ) def test_get_attr(self): class Klass(object): diff --git a/djadmin2/tests/test_views.py b/djadmin2/tests/test_views.py index 273510b..604b965 100644 --- a/djadmin2/tests/test_views.py +++ b/djadmin2/tests/test_views.py @@ -1,7 +1,7 @@ from django.test import TestCase, override_settings from django.urls import reverse -from django.utils.encoding import force_text +from django.utils.encoding import force_str from .. import views @@ -27,4 +27,4 @@ class CustomLoginViewTest(TestCase): def test_view_ok(self): response = self.client.get(reverse("admin2:dashboard")) - self.assertInHTML('

Custom login view

', force_text(response.content)) + self.assertInHTML('

Custom login view

', force_str(response.content)) diff --git a/djadmin2/tests/urls.py b/djadmin2/tests/urls.py index 283ce87..bb5e960 100644 --- a/djadmin2/tests/urls.py +++ b/djadmin2/tests/urls.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf import settings from django.conf.urls import url from django.conf.urls.static import static diff --git a/djadmin2/types.py b/djadmin2/types.py index 0750ead..d20343a 100644 --- a/djadmin2/types.py +++ b/djadmin2/types.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - import logging import os import sys @@ -10,7 +7,6 @@ import extra_views from django.conf.urls import url from django.forms import modelform_factory from django.urls import reverse -from django.utils.six import with_metaclass from . import actions from . import apiviews @@ -39,7 +35,7 @@ class ModelAdminBase2(type): return new_class -class ModelAdmin2(with_metaclass(ModelAdminBase2)): +class ModelAdmin2(metaclass=ModelAdminBase2): """ Adding new ModelAdmin2 attributes: @@ -320,7 +316,6 @@ def immutable_admin_factory(model_admin): 'workaround/hack' will read our documentation. """ ImmutableAdmin = namedtuple('ImmutableAdmin', - model_admin.model_admin_attributes, - verbose=False) + model_admin.model_admin_attributes) return ImmutableAdmin(*[getattr( model_admin, x) for x in model_admin.model_admin_attributes]) diff --git a/djadmin2/utils.py b/djadmin2/utils.py index 40a50db..fd46b93 100644 --- a/djadmin2/utils.py +++ b/djadmin2/utils.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from collections import defaultdict +from django.core.exceptions import FieldDoesNotExist +from django.db.models.constants import LOOKUP_SEP from django.db.models.deletion import Collector, ProtectedError -from django.db.models.sql.constants import QUERY_TERMS -from django.utils import six -from django.utils.encoding import force_bytes, force_text +from django.utils.encoding import force_bytes, force_str def lookup_needs_distinct(opts, lookup_path): @@ -19,20 +16,24 @@ def lookup_needs_distinct(opts, lookup_path): https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L22 """ - lookup_fields = lookup_path.split('__') - # Remove the last item of the lookup path if it is a query term - if lookup_fields[-1] in QUERY_TERMS: - lookup_fields = lookup_fields[:-1] - # Now go through the fields (following all relations) and look for an m2m + lookup_fields = lookup_path.split(LOOKUP_SEP) + # Go through the fields (following all relations) and look for an m2m. for field_name in lookup_fields: - field = opts.get_field(field_name) - if hasattr(field, 'get_path_info'): - # This field is a relation, update opts to follow the relation - path_info = field.get_path_info() - opts = path_info[-1].to_opts - if any(path.m2m for path in path_info): - # This field is a m2m relation so we know we need to call distinct - return True + if field_name == 'pk': + field_name = opts.pk.name + try: + field = opts.get_field(field_name) + except FieldDoesNotExist: + # Ignore query lookups. + continue + else: + if hasattr(field, 'get_path_info'): + # This field is a relation; update opts to follow the relation. + path_info = field.get_path_info() + opts = path_info[-1].to_opts + if any(path.m2m for path in path_info): + # This field is a m2m relation so distinct must be called. + return True return False @@ -142,9 +143,17 @@ class NestedObjects(Collector): except ProtectedError as e: self.protected.update(e.protected_objects) - def related_objects(self, related, objs): - qs = super(NestedObjects, self).related_objects(related, objs) - return qs.select_related(related.field.name) + def related_objects(self, *args): + # Django >= 3.1 + if len(args) == 3: + related_model, related_fields, objs = args + qs = super().related_objects(related_model, related_fields, objs) + return qs.select_related(*[related_field.name for related_field in related_fields]) + # Django < 3.1 + elif len(args) == 2: + related, objs = args + qs = super(NestedObjects, self).related_objects(related, objs) + return qs.select_related(related.field.name) def _nested(self, obj, seen, format_callback): if obj in seen: @@ -191,7 +200,7 @@ def quote(s): https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L66-L73 """ - if not isinstance(s, six.string_types): + if not isinstance(s, str): return s res = list(s) for i in range(len(res)): @@ -202,7 +211,4 @@ def quote(s): def type_str(text): - if six.PY2: - return force_bytes(text) - else: - return force_text(text) + return force_str(text) diff --git a/djadmin2/viewmixins.py b/djadmin2/viewmixins.py index 1e7e306..5a02cd7 100644 --- a/djadmin2/viewmixins.py +++ b/djadmin2/viewmixins.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - import os from django.contrib.auth.views import redirect_to_login @@ -8,7 +5,7 @@ from django.core.exceptions import PermissionDenied from django.forms.models import modelform_factory from django.http import HttpResponseRedirect from django.urls import reverse, reverse_lazy -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.text import get_text_list from django.utils.translation import ugettext as _ @@ -84,6 +81,9 @@ class Admin2Mixin(PermissionMixin): return [os.path.join( settings.ADMIN2_THEME_DIRECTORY, self.default_template_name)] + def get_templates(self): + return os.path.join(settings.ADMIN2_THEME_DIRECTORY, self.default_template_name) + def get_model(self): return self.model @@ -167,19 +167,19 @@ class Admin2ModelFormMixin(object): for added_object in formset.new_objects: change_message.append( _('Added {0} "{1}".'.format( - force_text(added_object._meta.verbose_name), - force_text(added_object)))) + force_str(added_object._meta.verbose_name), + force_str(added_object)))) for changed_object, changed_fields in formset.changed_objects: change_message.append( _('Changed {0} for {1} "{2}".'.format( get_text_list(changed_fields, _('and')), - force_text(changed_object._meta.verbose_name), - force_text(changed_object)))) + force_str(changed_object._meta.verbose_name), + force_str(changed_object)))) for deleted_object in formset.deleted_objects: change_message.append( _('Deleted {0} "{1}".'.format( - force_text(deleted_object._meta.verbose_name), - force_text(deleted_object)))) + force_str(deleted_object._meta.verbose_name), + force_str(deleted_object)))) change_message = ' '.join(change_message) return change_message or _('No fields changed.') diff --git a/djadmin2/views.py b/djadmin2/views.py index e0a134b..ac16a8e 100644 --- a/djadmin2/views.py +++ b/djadmin2/views.py @@ -1,25 +1,22 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - import operator from datetime import datetime from functools import reduce import extra_views +from django.core.exceptions import FieldDoesNotExist from django.conf import settings -from django.contrib.auth import update_session_auth_hash +from django.contrib.auth import (logout as auth_logout, + update_session_auth_hash) from django.contrib.auth.forms import (PasswordChangeForm, AdminPasswordChangeForm) -from django.contrib.auth.views import (logout as auth_logout, - login as auth_login) +from django.contrib.auth.views import LoginView as DjangoLoginView from django.contrib.contenttypes.models import ContentType from django.db import models, router -from django.db.models.fields import FieldDoesNotExist from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.text import capfirst -from django.utils.translation import ugettext_lazy +from django.utils.translation import gettext_lazy from django.urls import reverse, reverse_lazy from django.views import generic @@ -285,7 +282,7 @@ class ModelListView(Admin2ModelMixin, generic.ListView): elif year: context["previous_date"] = { "link": "?", - "text": ugettext_lazy("‹ All dates"), + "text": gettext_lazy("‹ All dates"), } context["dates"] = self._format_months(self.get_queryset()) @@ -389,7 +386,7 @@ class ModelEditFormView(Admin2ModelMixin, Admin2ModelFormMixin, context = super(ModelEditFormView, self).get_context_data(**kwargs) context['model'] = self.get_model() context['action'] = "Change" - context['action_name'] = ugettext_lazy("Change") + context['action_name'] = gettext_lazy("Change") return context def forms_valid(self, form, inlines): @@ -425,7 +422,7 @@ class ModelAddFormView(Admin2ModelMixin, Admin2ModelFormMixin, context = super(ModelAddFormView, self).get_context_data(**kwargs) context['model'] = self.get_model() context['action'] = "Add" - context['action_name'] = ugettext_lazy("Add") + context['action_name'] = gettext_lazy("Add") return context def forms_valid(self, form, inlines): @@ -462,8 +459,8 @@ class ModelDeleteView(Admin2ModelMixin, generic.DeleteView): def _format_callback(obj): opts = utils.model_options(obj) - return '%s: %s' % (force_text(capfirst(opts.verbose_name)), - force_text(obj)) + return '%s: %s' % (force_str(capfirst(opts.verbose_name)), + force_str(obj)) using = router.db_for_write(self.get_object()._meta.model) collector = utils.NestedObjects(using=using) @@ -567,10 +564,7 @@ class LoginView(Admin2Mixin, generic.TemplateView): authentication_form = AdminAuthenticationForm def dispatch(self, request, *args, **kwargs): - return auth_login(request, - authentication_form=self.authentication_form, - template_name=self.get_template_names(), - *args, **kwargs) + return DjangoLoginView.as_view(template_name=self.get_templates(), authentication_form=self.authentication_form, *args, **kwargs)(request) class LogoutView(Admin2Mixin, generic.TemplateView): @@ -582,5 +576,6 @@ class LogoutView(Admin2Mixin, generic.TemplateView): default_template_name = 'auth/logout.html' def get(self, request, *args, **kwargs): - return auth_logout(request, template_name=self.get_template_names(), - *args, **kwargs) + auth_logout(request) + context = self.get_context_data(**kwargs) + return self.render_to_response(context) diff --git a/docs/conf.py b/docs/conf.py index 4b5b88d..4d21936 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # django-admin2 documentation build configuration file, created by # sphinx-quickstart on Sat May 18 12:59:02 2013. # diff --git a/docs/internationalization.rst b/docs/internationalization.rst index 9bfeb28..252132e 100644 --- a/docs/internationalization.rst +++ b/docs/internationalization.rst @@ -71,7 +71,7 @@ Marking strings for translation **Python code** -Make sure to use ugettext or ugettext_lazy on strings that will be shown to the users, +Make sure to use ugettext or gettext_lazy on strings that will be shown to the users, with string interpolation ( "%(name_of_variable)s" instead of "%s" ) where needed. Remember that all languages do not use the same word order, so try to provide flexible strings to translate ! diff --git a/docs/ref/themes.rst b/docs/ref/themes.rst index 0779aad..743aafd 100644 --- a/docs/ref/themes.rst +++ b/docs/ref/themes.rst @@ -37,7 +37,6 @@ Then enter the following information (you will probably want to change the highl #!/usr/bin/env python - # -*- coding: utf-8 -*- from setuptools import setup import re diff --git a/example/blog/actions.py b/example/blog/actions.py index 6c7aa10..c8a667e 100644 --- a/example/blog/actions.py +++ b/example/blog/actions.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.contrib import messages -from django.utils.translation import ugettext_lazy, pgettext_lazy +from django.utils.translation import gettext_lazy, pgettext_lazy from djadmin2 import permissions from djadmin2.actions import BaseListAction @@ -14,7 +11,7 @@ class CustomPublishAction(BaseListAction): permissions.ModelChangePermission, ) - description = ugettext_lazy('Publish selected items') + description = gettext_lazy('Publish selected items') success_message = pgettext_lazy( 'singular form', 'Successfully published %(count)s %(items)s') @@ -33,7 +30,7 @@ class PublishAllItemsAction(BaseListAction): permissions.ModelChangePermission, ) - description = ugettext_lazy('Publish all items') + description = gettext_lazy('Publish all items') success_message = pgettext_lazy( 'singular form', 'Successfully published %(count)s %(items)s', @@ -54,11 +51,11 @@ class PublishAllItemsAction(BaseListAction): def unpublish_items(request, queryset): queryset.update(published=False) messages.add_message(request, messages.INFO, - ugettext_lazy(u'Items unpublished')) + gettext_lazy(u'Items unpublished')) # Translators : action description -unpublish_items.description = ugettext_lazy('Unpublish selected items') +unpublish_items.description = gettext_lazy('Unpublish selected items') def unpublish_all_items(request, queryset): @@ -66,9 +63,9 @@ def unpublish_all_items(request, queryset): messages.add_message( request, messages.INFO, - ugettext_lazy('Items unpublished'), + gettext_lazy('Items unpublished'), ) -unpublish_all_items.description = ugettext_lazy('Unpublish all items') +unpublish_all_items.description = gettext_lazy('Unpublish all items') unpublish_all_items.only_selected = False diff --git a/example/blog/admin.py b/example/blog/admin.py index 409cb58..e1fc421 100644 --- a/example/blog/admin.py +++ b/example/blog/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.contrib import admin from .models import Post, Comment diff --git a/example/blog/admin2.py b/example/blog/admin2.py index 89be59b..9a10b0b 100644 --- a/example/blog/admin2.py +++ b/example/blog/admin2.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals -from django.utils.translation import ugettext_lazy +from django.utils.translation import gettext_lazy from djadmin2 import renderers from djadmin2.actions import DeleteSelectedAction @@ -46,7 +44,7 @@ class CommentAdmin(ModelAdmin2): # Register the blog app with a verbose name djadmin2_site.register_app_verbose_name( 'blog', - ugettext_lazy('My Blog') + gettext_lazy('My Blog') ) # Register each model with the admin diff --git a/example/blog/migrations/0001_initial.py b/example/blog/migrations/0001_initial.py index a063401..95f4309 100644 --- a/example/blog/migrations/0001_initial.py +++ b/example/blog/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models import django.db.models.deletion diff --git a/example/blog/models.py b/example/blog/models.py index c5bfd10..89ac5d9 100644 --- a/example/blog/models.py +++ b/example/blog/models.py @@ -1,13 +1,7 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.db import models -from django.utils import six -from django.utils.encoding import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ -@python_2_unicode_compatible class Post(models.Model): title = models.CharField(max_length=255, verbose_name=_('title')) body = models.TextField(verbose_name=_('body')) @@ -22,7 +16,6 @@ class Post(models.Model): verbose_name_plural = _('posts') -@python_2_unicode_compatible class Comment(models.Model): post = models.ForeignKey( Post, verbose_name=_('post'), related_name="comments", @@ -39,13 +32,12 @@ class Comment(models.Model): # Models needed for testing NestedObjects -@python_2_unicode_compatible class Count(models.Model): num = models.PositiveSmallIntegerField() parent = models.ForeignKey('self', null=True, on_delete=models.CASCADE) def __str__(self): - return six.text_type(self.num) + return str(self.num) class Event(models.Model): diff --git a/example/blog/tests/test_apiviews.py b/example/blog/tests/test_apiviews.py index 4d3fab8..aa72eab 100644 --- a/example/blog/tests/test_apiviews.py +++ b/example/blog/tests/test_apiviews.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import json from django.contrib.auth.models import AnonymousUser, User @@ -7,7 +5,7 @@ from django.core.exceptions import PermissionDenied from django.test import TestCase from django.test.client import RequestFactory from django.urls import reverse -from django.utils.encoding import force_text +from django.utils.encoding import force_str from djadmin2 import apiviews from djadmin2.site import djadmin2_site @@ -74,7 +72,7 @@ class ListCreateAPIViewTest(APITestCase): response.render() self.assertEqual(response.status_code, 200) - self.assertIn('"__unicode__":"Foo"', force_text(response.content)) + self.assertIn('"__unicode__":"Foo"', force_str(response.content)) def test_pagination(self): request = self.factory.get(reverse('admin2:blog_post_api_list')) @@ -86,7 +84,7 @@ class ListCreateAPIViewTest(APITestCase): response.render() self.assertEqual(response.status_code, 200) - data = json.loads(force_text(response.content)) + data = json.loads(force_str(response.content)) self.assertEqual(data['count'], 0) # next and previous fields exist, but are null because we have no # content diff --git a/example/blog/tests/test_filters.py b/example/blog/tests/test_filters.py index ffac928..716af06 100644 --- a/example/blog/tests/test_filters.py +++ b/example/blog/tests/test_filters.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -# vim:fenc=utf-8 - import django_filters from django.test import TestCase from django.test.client import RequestFactory @@ -44,19 +41,11 @@ class ListFilterBuilderTest(TestCase): self.assertTrue( issubclass(list_filter_inst.__class__, django_filters.FilterSet) ) - self.assertEqual( - list_filter_inst.filters['published'].widget, - djadmin2_filters.NullBooleanLinksWidget, - ) list_filter_inst = djadmin2_filters.build_list_filter( request, PostAdminWithFilterInstances, Post.objects.all(), ) - self.assertNotEqual( - list_filter_inst.filters['published'].widget, - djadmin2_filters.NullBooleanLinksWidget, - ) list_filter_inst = djadmin2_filters.build_list_filter( request, PostAdminWithFilterSetInst, diff --git a/example/blog/tests/test_modelforms.py b/example/blog/tests/test_modelforms.py index 7f98a98..bc9c223 100644 --- a/example/blog/tests/test_modelforms.py +++ b/example/blog/tests/test_modelforms.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django import forms from django.forms import modelform_factory from django.test import TestCase diff --git a/example/blog/tests/test_views.py b/example/blog/tests/test_views.py index db73ac3..c52c119 100644 --- a/example/blog/tests/test_views.py +++ b/example/blog/tests/test_views.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from datetime import datetime from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.test import TestCase, Client from django.urls import reverse -from django.utils.encoding import force_text +from django.utils.encoding import force_str from ..models import Post, Comment @@ -140,7 +137,7 @@ class PostListTest(BaseIntegrationTest): def test_actions_displayed(self): response = self.client.get(reverse("admin2:blog_post_index")) self.assertInHTML( - 'Delete selected items', force_text(response.content)) + 'Delete selected items', force_str(response.content)) def test_actions_displayed_twice(self): # If actions_on_top and actions_on_bottom are both set @@ -155,7 +152,7 @@ class PostListTest(BaseIntegrationTest): response = self.client.post(reverse("admin2:blog_post_index"), params) # caution : uses pluralization self.assertInHTML( - '

Are you sure you want to delete the selected post? The following item will be deleted:

', force_text(response.content)) + '

Are you sure you want to delete the selected post? The following item will be deleted:

', force_str(response.content)) def test_delete_selected_post_confirmation(self): post = Post.objects.create(title="A Post Title", body="body") @@ -331,7 +328,7 @@ class PostListTestCustomAction(BaseIntegrationTest): def test_publish_action_displayed_in_list(self): response = self.client.get(reverse("admin2:blog_post_index")) self.assertInHTML( - 'Publish selected items', force_text(response.content)) + 'Publish selected items', force_str(response.content)) def test_publish_selected_items(self): post = Post.objects.create(title="A Post Title", @@ -350,7 +347,7 @@ class PostListTestCustomAction(BaseIntegrationTest): def test_unpublish_action_displayed_in_list(self): response = self.client.get(reverse("admin2:blog_post_index")) self.assertInHTML( - 'Unpublish selected items', force_text(response.content)) + 'Unpublish selected items', force_str(response.content)) def test_unpublish_selected_items(self): post = Post.objects.create(title="A Post Title", @@ -380,7 +377,7 @@ class PostCreateViewTest(BaseIntegrationTest): def test_view_ok(self): response = self.client.get(reverse("admin2:blog_post_create")) self.assertNotIn( - '''enctype="multipart/form-data"''', force_text(response.content)) + '''enctype="multipart/form-data"''', force_str(response.content)) self.assertEqual(response.status_code, 200) def test_create_post(self): diff --git a/example/example/urls.py b/example/example/urls.py index eb45787..21655bb 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from blog.views import BlogListView, BlogDetailView from django.conf import settings from django.conf.urls import url diff --git a/example/files/admin.py b/example/files/admin.py index 1629d51..6319e3a 100644 --- a/example/files/admin.py +++ b/example/files/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.contrib import admin from .models import CaptionedFile, UncaptionedFile diff --git a/example/files/migrations/0001_initial.py b/example/files/migrations/0001_initial.py index fb58015..7ef76ef 100644 --- a/example/files/migrations/0001_initial.py +++ b/example/files/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/example/files/models.py b/example/files/models.py index 966c611..ea762c3 100644 --- a/example/files/models.py +++ b/example/files/models.py @@ -1,12 +1,7 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.db import models -from django.utils.encoding import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ -@python_2_unicode_compatible class CaptionedFile(models.Model): caption = models.CharField(max_length=200, verbose_name=_('caption')) publication = models.FileField(upload_to='captioned-files', verbose_name=_('Uploaded File')) @@ -19,7 +14,6 @@ class CaptionedFile(models.Model): verbose_name_plural = _('Captioned Files') -@python_2_unicode_compatible class UncaptionedFile(models.Model): publication = models.FileField(upload_to='uncaptioned-files', verbose_name=_('Uploaded File')) diff --git a/example/files/tests/test_views.py b/example/files/tests/test_views.py index 0d743a0..e808e30 100644 --- a/example/files/tests/test_views.py +++ b/example/files/tests/test_views.py @@ -3,7 +3,7 @@ from os import path from django.contrib.auth import get_user_model from django.urls import reverse from django.test import TestCase, Client -from django.utils.encoding import force_text +from django.utils.encoding import force_str from ..models import CaptionedFile @@ -45,7 +45,7 @@ class CaptionedFileListTest(BaseIntegrationTest): def test_actions_displayed(self): response = self.client.get(reverse("admin2:files_captionedfile_index")) self.assertInHTML( - 'Delete selected items', force_text(response.content)) + 'Delete selected items', force_str(response.content)) def test_delete_selected_captioned_file(self): captioned_file = CaptionedFile.objects.create( @@ -55,7 +55,7 @@ class CaptionedFileListTest(BaseIntegrationTest): response = self.client.post( reverse("admin2:files_captionedfile_index"), params) self.assertInHTML( - '

Are you sure you want to delete the selected Captioned File? The following item will be deleted:

', force_text(response.content)) + '

Are you sure you want to delete the selected Captioned File? The following item will be deleted:

', force_str(response.content)) def test_delete_selected_captioned_file_confirmation(self): captioned_file = CaptionedFile.objects.create( @@ -93,7 +93,7 @@ class CaptionedFileCreateViewTest(BaseIntegrationTest): response = self.client.get( reverse("admin2:files_captionedfile_create")) self.assertIn( - 'enctype="multipart/form-data"', force_text(response.content)) + 'enctype="multipart/form-data"', force_str(response.content)) self.assertEqual(response.status_code, 200) def test_create_captioned_file(self): diff --git a/example/polls/admin.py b/example/polls/admin.py index 2a645d5..2969ca4 100644 --- a/example/polls/admin.py +++ b/example/polls/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from django.contrib import admin from .models import Poll, Choice diff --git a/example/polls/admin2.py b/example/polls/admin2.py index a532b9e..f39ec0f 100644 --- a/example/polls/admin2.py +++ b/example/polls/admin2.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - from djadmin2.site import djadmin2_site from djadmin2.types import Admin2TabularInline, ModelAdmin2 from .models import Poll, Choice diff --git a/example/polls/migrations/0001_initial.py b/example/polls/migrations/0001_initial.py index 8dd792e..c4eb624 100644 --- a/example/polls/migrations/0001_initial.py +++ b/example/polls/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/example/polls/models.py b/example/polls/models.py index a4d14f2..9a6fc52 100644 --- a/example/polls/models.py +++ b/example/polls/models.py @@ -1,15 +1,10 @@ -# -*- coding: utf-8 -*- -from __future__ import division, absolute_import, unicode_literals - import datetime from django.db import models from django.utils import timezone -from django.utils.encoding import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ -@python_2_unicode_compatible class Poll(models.Model): question = models.CharField(max_length=200, verbose_name=_('question')) pub_date = models.DateTimeField(verbose_name=_('date published')) @@ -28,7 +23,6 @@ class Poll(models.Model): verbose_name_plural = _('polls') -@python_2_unicode_compatible class Choice(models.Model): poll = models.ForeignKey( Poll, diff --git a/example/polls/tests/test_views.py b/example/polls/tests/test_views.py index a2bcc65..4f0b37b 100644 --- a/example/polls/tests/test_views.py +++ b/example/polls/tests/test_views.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.test import TestCase, Client from django.urls import reverse from django.utils import timezone -from django.utils.encoding import force_text +from django.utils.encoding import force_str from ..models import Poll @@ -40,7 +40,7 @@ class PollListTest(BaseIntegrationTest): def test_actions_displayed(self): response = self.client.get(reverse("admin2:polls_poll_index")) self.assertInHTML( - 'Delete selected items', force_text(response.content)) + 'Delete selected items', force_str(response.content)) def test_delete_selected_poll(self): poll = Poll.objects.create( @@ -49,7 +49,7 @@ class PollListTest(BaseIntegrationTest): 'selected_model_pk': str(poll.pk)} response = self.client.post(reverse("admin2:polls_poll_index"), params) self.assertInHTML( - '

Are you sure you want to delete the selected poll? The following item will be deleted:

', force_text(response.content)) + '

Are you sure you want to delete the selected poll? The following item will be deleted:

', force_str(response.content)) def test_delete_selected_poll_confirmation(self): poll = Poll.objects.create( diff --git a/fabfile.py b/fabfile.py index f5674ee..af81449 100644 --- a/fabfile.py +++ b/fabfile.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function, division, absolute_import, unicode_literals - from fabric.api import local, lcd from fabric.contrib.console import confirm diff --git a/requirements.txt b/requirements.txt index f16ae34..1c4cc38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ django-extra-views==0.12.0 -django-braces==1.13.0 -djangorestframework==3.9.2 -django-filter==1.1.0 +django-braces==1.14.0 +djangorestframework==3.11.1 +django-filter==2.3.0 django-debug-toolbar>=1.10.1 future>=0.15.2 pytz>=2016.4 diff --git a/setup.py b/setup.py index edface3..7851de5 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- from setuptools import setup from setuptools.command.test import test as TestCommand @@ -131,11 +130,11 @@ setup( include_package_data=True, #test_suite='runtests.runtests', install_requires=[ - 'django>=1.11.1', + 'django>=2.2', 'django-extra-views>=0.12.0', 'django-braces>=1.3.0', - 'djangorestframework>=3.9.0', - 'django-filter==1.1.0', + 'djangorestframework>=3.11.1', + 'django-filter==2.3.0', 'pytz>=2016.4', 'future>=0.15.2', ], diff --git a/tox.ini b/tox.ini index 36d60ad..f8c876d 100644 --- a/tox.ini +++ b/tox.ini @@ -6,18 +6,20 @@ exclude = migrations/*,docs/* [tox] envlist = - py27-{1.11}, - py35-{1.11,2.0,2.1}, - py36-{2.0,2.1,master}, + py35-{2.2,3.0}, + py36-{2.2,3.0,3.1}, + py37-{2.2,3.0,3.1,master}, + py38-{2.2,3.0,3.1,master}, + [testenv] commands = py.test [] deps = -rrequirements_test.txt - 2.1: Django>=2.1,<2.2 - 1.11: Django>=1.11,<2.0 - 2.0: Django>=2.0,<2.1 + 3.1: Django>=3.1,<3.2 + 3.0: Django>=3.0,<3.1 + 2.2: Django>=2.2,<2.3 master: https://github.com/django/django/tarball/master setenv= DJANGO_SETTINGS_MODULE = example.settings