Merge pull request #167 from PetrDlouhy/django40

add support for Django 4.0 and 4.1, Python 3.10, drop support of Django 1.11
This commit is contained in:
Corey Oordt 2022-09-21 11:14:01 -05:00 committed by GitHub
commit 68abe74741
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 96 additions and 70 deletions

View file

@ -13,7 +13,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
python-version: [3.10]
fail-fast: false
steps:
- uses: actions/checkout@v2

View file

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ 3.6, 3.7, 3.8, 3.9 ]
python-version: [ 3.6, 3.7, 3.8, 3.9, '3.10' ]
steps:
- uses: actions/checkout@v1

View file

@ -5,7 +5,7 @@ repos:
- id: isort
additional_dependencies: [toml]
- repo: https://github.com/python/black
rev: 21.11b1
rev: 22.8.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks

View file

@ -1,7 +1,7 @@
"""Admin interface classes."""
from django import forms
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from .base import CategoryBaseAdmin, CategoryBaseAdminForm
from .genericcollection import GenericCollectionTabularInline

View file

@ -6,8 +6,8 @@ It provides customizable metadata and its own name space.
from django import forms
from django.contrib import admin
from django.db import models
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from mptt.fields import TreeForeignKey
from mptt.managers import TreeManager
from mptt.models import MPTTModel
@ -77,7 +77,7 @@ class CategoryBase(MPTTModel):
def __str__(self):
ancestors = self.get_ancestors()
return " > ".join(
[force_text(i.name) for i in ancestors]
[force_str(i.name) for i in ancestors]
+ [
self.name,
]

View file

@ -5,7 +5,7 @@ from django.contrib.admin.utils import lookup_field
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.template import Library
from django.utils.encoding import force_text, smart_text
from django.utils.encoding import force_str, smart_str
from django.utils.html import conditional_escape, escape, escapejs, format_html
from django.utils.safestring import mark_safe
@ -53,7 +53,7 @@ def items_for_tree_result(cl, result, form):
allow_tags = True
result_repr = _boolean_icon(value)
else:
result_repr = smart_text(value)
result_repr = smart_str(value)
# Strip HTML tags in the resulting text, except if the
# function has an "allow_tags" attribute set to True.
if not allow_tags:
@ -80,7 +80,7 @@ def items_for_tree_result(cl, result, form):
except (AttributeError, ObjectDoesNotExist):
pass
if force_text(result_repr) == "":
if force_str(result_repr) == "":
result_repr = mark_safe(" ")
# If list_display_links not defined, add the link tag to the first field
if (first and not cl.list_display_links) or field_name in cl.list_display_links:
@ -97,12 +97,12 @@ def items_for_tree_result(cl, result, form):
else:
attr = pk
value = result.serializable_value(attr)
result_id = repr(force_text(value))[1:]
result_id = repr(force_str(value))[1:]
first = False
result_id = escapejs(value)
yield mark_safe(
format_html(
smart_text('<{}{}><a href="{}"{}>{}</a></{}>'),
smart_str('<{}{}><a href="{}"{}>{}</a></{}>'),
table_tag,
row_class,
url,
@ -123,12 +123,12 @@ def items_for_tree_result(cl, result, form):
# can provide fields on a per request basis
if form and field_name in form.fields:
bf = form[field_name]
result_repr = mark_safe(force_text(bf.errors) + force_text(bf))
result_repr = mark_safe(force_str(bf.errors) + force_str(bf))
else:
result_repr = conditional_escape(result_repr)
yield mark_safe(smart_text("<td%s>%s</td>" % (row_class, result_repr)))
yield mark_safe(smart_str("<td%s>%s</td>" % (row_class, result_repr)))
if form and not form[cl.model._meta.pk.name].is_hidden:
yield mark_safe(smart_text("<td>%s</td>" % force_text(form[cl.model._meta.pk.name])))
yield mark_safe(smart_str("<td>%s</td>" % force_str(form[cl.model._meta.pk.name])))
class TreeList(list):

View file

@ -8,7 +8,7 @@ from django.contrib.admin.views.main import ChangeList
from django.db.models.query import QuerySet
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from . import settings
@ -150,8 +150,8 @@ class TreeEditor(admin.ModelAdmin):
"""The 'change list' admin view for this model."""
from django.contrib.admin.views.main import ERROR_FLAG
from django.core.exceptions import PermissionDenied
from django.utils.encoding import force_text
from django.utils.translation import ungettext
from django.utils.encoding import force_str
from django.utils.translation import ngettext
opts = self.model._meta
app_label = opts.app_label
@ -199,6 +199,22 @@ class TreeEditor(admin.ModelAdmin):
self.list_editable,
self,
)
elif django.VERSION < (4, 0):
params = (
request,
self.model,
list_display,
self.list_display_links,
self.list_filter,
self.date_hierarchy,
self.search_fields,
self.list_select_related,
self.list_per_page,
self.list_max_show_all,
self.list_editable,
self,
self.sortable_by,
)
else:
params = (
request,
@ -214,6 +230,7 @@ class TreeEditor(admin.ModelAdmin):
self.list_editable,
self,
self.sortable_by,
self.search_help_text,
)
cl = TreeChangeList(*params)
except IncorrectLookupParameters:
@ -256,17 +273,14 @@ class TreeEditor(admin.ModelAdmin):
if changecount:
if changecount == 1:
name = force_text(opts.verbose_name)
name = force_str(opts.verbose_name)
else:
name = force_text(opts.verbose_name_plural)
msg = (
ungettext(
"%(count)s %(name)s was changed successfully.",
"%(count)s %(name)s were changed successfully.",
changecount,
)
% {"count": changecount, "name": name, "obj": force_text(obj)}
)
name = force_str(opts.verbose_name_plural)
msg = ngettext(
"%(count)s %(name)s was changed successfully.",
"%(count)s %(name)s were changed successfully.",
changecount,
) % {"count": changecount, "name": name, "obj": force_str(obj)}
self.message_user(request, msg)
return HttpResponseRedirect(request.get_full_path())
@ -303,11 +317,11 @@ class TreeEditor(admin.ModelAdmin):
if django.VERSION[0] == 1 and django.VERSION[1] < 4:
context["root_path"] = self.admin_site.root_path
elif django.VERSION[0] == 1 or (django.VERSION[0] == 2 and django.VERSION[1] < 1):
selection_note_all = ungettext("%(total_count)s selected", "All %(total_count)s selected", cl.result_count)
selection_note_all = ngettext("%(total_count)s selected", "All %(total_count)s selected", cl.result_count)
context.update(
{
"module_name": force_text(opts.verbose_name_plural),
"module_name": force_str(opts.verbose_name_plural),
"selection_note": _("0 of %(cnt)s selected") % {"cnt": len(cl.result_list)},
"selection_note_all": selection_note_all % {"total_count": cl.result_count},
}

View file

@ -10,7 +10,7 @@ class Command(BaseCommand):
help = "Alter the tables for all registered models, or just specified models"
args = "[appname ...]"
can_import_settings = True
requires_system_checks = False
requires_system_checks = []
def add_arguments(self, parser):
"""Add app_names argument to the command."""

View file

@ -10,7 +10,7 @@ class Command(BaseCommand):
help = "Drop the given field from the given model's table"
args = "appname modelname fieldname"
can_import_settings = True
requires_system_checks = False
requires_system_checks = []
def add_arguments(self, parser):
"""Add app_name, model_name, and field_name arguments to the command."""

View file

@ -1,7 +1,7 @@
"""Adds and removes category relations on the database."""
from django.apps import apps
from django.db import connection, transaction
from django.db.utils import ProgrammingError
from django.db import DatabaseError, connection, transaction
from django.db.utils import OperationalError, ProgrammingError
def table_exists(table_name):
@ -25,7 +25,10 @@ def field_exists(app_name, model_name, field_name):
field = model._meta.get_field(field_name)
if hasattr(field, "m2m_db_table"):
m2m_table_name = field.m2m_db_table()
m2m_field_info = connection.introspection.get_table_description(cursor, m2m_table_name)
try:
m2m_field_info = connection.introspection.get_table_description(cursor, m2m_table_name)
except DatabaseError: # Django >= 4.1 throws DatabaseError
m2m_field_info = []
if m2m_field_info:
return True
@ -68,7 +71,9 @@ def migrate_app(sender, *args, **kwargs):
schema_editor.add_field(model, registry._field_registry[fld])
if sid:
transaction.savepoint_commit(sid)
except ProgrammingError:
# Django 4.1 with sqlite3 has for some reason started throwing OperationalError
# instead of ProgrammingError, so we need to catch both.
except (ProgrammingError, OperationalError):
if sid:
transaction.savepoint_rollback(sid)
continue

View file

@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.files.images import get_image_dimensions
from django.db import models
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.encoding import force_str
try:
from django.contrib.contenttypes.fields import GenericForeignKey
@ -13,7 +13,7 @@ except ImportError:
from django.contrib.contenttypes.generic import GenericForeignKey
from django.core.files.storage import get_storage_class
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from .base import CategoryBase
from .settings import (
@ -72,7 +72,7 @@ class Category(CategoryBase):
ancestors = list(self.get_ancestors()) + [
self,
]
return prefix + "/".join([force_text(i.slug) for i in ancestors]) + "/"
return prefix + "/".join([force_str(i.slug) for i in ancestors]) + "/"
if RELATION_MODELS:

View file

@ -3,13 +3,13 @@ These functions handle the adding of fields to other models.
"""
from typing import Optional, Type, Union
import collections
from collections.abc import Iterable
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db.models import CASCADE, ForeignKey, ManyToManyField
# from settings import self._field_registry, self._model_registry
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from . import fields
@ -26,9 +26,7 @@ class Registry(object):
self._field_registry = {}
self._model_registry = {}
def register_model(
self, app: str, model_name, field_type: str, field_definitions: Union[str, collections.Iterable]
):
def register_model(self, app: str, model_name, field_type: str, field_definitions: Union[str, Iterable]):
"""
Registration process for Django 1.7+.
@ -41,15 +39,13 @@ class Registry(object):
Raises:
ImproperlyConfigured: For incorrect parameter types or missing model.
"""
import collections
from django.apps import apps
app_label = app
if isinstance(field_definitions, str):
field_definitions = [field_definitions]
elif not isinstance(field_definitions, collections.Iterable):
elif not isinstance(field_definitions, Iterable):
raise ImproperlyConfigured(
_("Field configuration for %(app)s should be a string or iterable") % {"app": app}
)

View file

@ -3,7 +3,7 @@ import collections
from django.conf import settings
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
DEFAULT_SETTINGS = {
"ALLOW_SLUG_CHANGE": False,

View file

@ -43,12 +43,12 @@
<p>{{ field.contents }}</p>
{% else %}
{{ field.field.errors.as_ul }}
{% ifequal field.field.name inline_admin_formset.formset.ct_fk_field %}
{% if field.field.name == inline_admin_formset.formset.ct_fk_field %}
{{ field.field }}
<a id="lookup_id_{{field.field.html_name}}" class="related-lookup" onclick="return showGenericRelatedObjectLookupPopup(this, {{ inline_admin_formset.formset.content_types }});" href="#">
<img width="16" height="16" alt="Lookup" src="{% static 'img/admin/selector-search.gif' %}"/>
</a>
{% else %}{{ field.field }} {% endifequal %}
{% else %}{{ field.field }} {% endif %}
{% endif %}
</td>
{% endfor %}

View file

@ -4,8 +4,8 @@
{% if structure.new_level %}<ul><li>
{% else %}</li><li>
{% endif %}
{% ifequal node category %}<strong>{{ node.name }}</strong>
{% if node == category %}<strong>{{ node.name }}</strong>
{% else %}<a href="{{ node.get_absolute_url }}">{{ node.name }}</a>
{% endifequal %}
{% endif %}
{% for level in structure.closed_levels %}</li></ul>{% endfor %}
{% endfor %}</li></ul>

View file

@ -4,8 +4,8 @@
{% if structure.new_level %}<ul><li>
{% else %}</li><li>
{% endif %}
{% ifequal node category %}<strong>{{ node.name }}</strong>
{% if node == category %}<strong>{{ node.name }}</strong>
{% else %}<a href="{{ node.get_absolute_url }}">{{ node.name }}</a>
{% endifequal %}
{% endif %}
{% for level in structure.closed_levels %}</li></ul>{% endfor %}
{% endfor %}</li></ul>{% endspaceless %}

View file

@ -1,7 +1,7 @@
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from django.utils.encoding import smart_text
from django.utils.encoding import smart_str
from categories.models import Category
@ -16,7 +16,7 @@ class TestCategoryAdmin(TestCase):
url = reverse("admin:categories_category_add")
data = {
"parent": "",
"name": smart_text("Parent Catégory"),
"name": smart_str("Parent Catégory"),
"thumbnail": "",
"filename": "",
"active": "on",
@ -38,7 +38,7 @@ class TestCategoryAdmin(TestCase):
self.assertEqual(1, Category.objects.count())
# update parent
data.update({"name": smart_text("Parent Catégory (Changed)")})
data.update({"name": smart_str("Parent Catégory (Changed)")})
resp = self.client.post(reverse("admin:categories_category_change", args=(1,)), data=data)
self.assertEqual(resp.status_code, 302)
self.assertEqual(1, Category.objects.count())
@ -47,8 +47,8 @@ class TestCategoryAdmin(TestCase):
data.update(
{
"parent": "1",
"name": smart_text("Child Catégory"),
"slug": smart_text("child-category"),
"name": smart_str("Child Catégory"),
"slug": smart_str("child-category"),
}
)
resp = self.client.post(url, data=data)

View file

@ -1,5 +1,5 @@
"""URL patterns for the categories app."""
from django.conf.urls import url
from django.urls import path, re_path
from django.views.generic import ListView
from . import views
@ -7,6 +7,6 @@ from .models import Category
categorytree_dict = {"queryset": Category.objects.filter(level=0)}
urlpatterns = (url(r"^$", ListView.as_view(**categorytree_dict), name="categories_tree_list"),)
urlpatterns = (path("", ListView.as_view(**categorytree_dict), name="categories_tree_list"),)
urlpatterns += (url(r"^(?P<path>.+)/$", views.category_detail, name="categories_category"),)
urlpatterns += (re_path(r"^(?P<path>.+)/$", views.category_detail, name="categories_category"),)

View file

@ -4,7 +4,7 @@ from typing import Optional
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import select_template
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from .models import Category

View file

@ -1,8 +1,9 @@
"""URL patterns for the example project."""
import os
from django.conf.urls import include, url
from django.conf.urls import include
from django.contrib import admin
from django.urls import path, re_path
from django.views.static import serve
admin.autodiscover()
@ -17,12 +18,14 @@ urlpatterns = (
# to INSTALLED_APPS to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
url(r"^admin/", admin.site.urls),
url(r"^categories/", include("categories.urls")),
path("admin/", admin.site.urls),
path("categories/", include("categories.urls")),
# r'^cats/', include('categories.urls')),
url(r"^static/categories/(?P<path>.*)$", serve, {"document_root": ROOT_PATH + "/categories/media/categories/"}),
re_path(
r"^static/categories/(?P<path>.*)$", serve, {"document_root": ROOT_PATH + "/categories/media/categories/"}
),
# (r'^static/editor/(?P<path>.*)$', 'django.views.static.serve',
# {'document_root': ROOT_PATH + '/editor/media/editor/',
# 'show_indexes':True}),
url(r"^static/(?P<path>.*)$", serve, {"document_root": os.path.join(ROOT_PATH, "example", "static")}),
re_path(r"^static/(?P<path>.*)$", serve, {"document_root": os.path.join(ROOT_PATH, "example", "static")}),
)

View file

@ -47,4 +47,5 @@ requirements = parse_reqs("requirements.txt")
setup(
install_requires=requirements,
py_modules=[],
)

10
tox.ini
View file

@ -2,7 +2,10 @@
envlist =
begin
py36-lint
py{36,37,38,39}-django{111,2,21,22,3,31}
py{36,37,38,39}-django{2}
py{36,37,38,39,310}-django{21,22,3,31,32}
py{38,39,310}-django{40}
py{38,39,310}-django{41}
coverage-report
[gh-actions]
@ -11,6 +14,7 @@ python =
3.7: py37
3.8: py38
3.9: py39
3.10: py310
[testenv]
passenv = GITHUB_*
@ -21,7 +25,9 @@ deps=
django22: Django>=2.2,<2.3
django3: Django>=3.0,<3.1
django31: Django>=3.1,<3.2
django111: Django>=1.11,<1.12
django32: Django>=3.2,<4.0
django40: Django>=4.0,<4.1
django41: Django>=4.1,<5.0
coverage[toml]
pillow
ipdb