Skip to content

Commit

Permalink
feat: Support django 5.1 (#756)
Browse files Browse the repository at this point in the history
Refs #719
  • Loading branch information
last-partizan authored Nov 11, 2024
1 parent 22eef09 commit 7a60ca1
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 29 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ jobs:
strategy:
matrix:
python: ["3.9", "3.10", "3.11", "3.12"]
django: ["4.2", "5.0"]
django: ["4.2", "5.0", "5.1"]
database: ["sqlite", "postgres", "mysql"]
exclude:
- python: 3.9
django: 5.0
- python: 3.9
django: 5.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DJANGO: ${{ matrix.django }}
Expand Down
31 changes: 31 additions & 0 deletions modeltranslation/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import django

if TYPE_CHECKING:
from django.db.models.fields.reverse_related import ForeignObjectRel


def is_hidden(field: ForeignObjectRel) -> bool:
return field.hidden


def clear_ForeignObjectRel_caches(field: ForeignObjectRel):
"""
Django 5.1 Introduced caching for `accessor_name` props.
We need to clear this cache when creating Translated field.
https://github.com/django/django/commit/5e80390add100e0c7a1ac8e51739f94c5d706ea3#diff-e65b05ecbbe594164125af53550a43ef8a174f80811608012bc8e9e4ed575749
"""
caches = ("accessor_name",)
for name in caches:
field.__dict__.pop(name, None)


if django.VERSION <= (5, 1):

def is_hidden(field: ForeignObjectRel) -> bool:
return field.is_hidden()
22 changes: 6 additions & 16 deletions modeltranslation/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from modeltranslation.widgets import ClearableWidgetWrapper

from ._typing import Self
from ._compat import is_hidden, clear_ForeignObjectRel_caches

SUPPORTED_FIELDS = (
fields.CharField,
Expand Down Expand Up @@ -173,6 +174,9 @@ def __init__(
# (will show up e.g. in the admin).
self.verbose_name = build_localized_verbose_name(translated_field.verbose_name, language)

if self.remote_field:
clear_ForeignObjectRel_caches(self.remote_field)

# M2M support - <rewrite related_name> <patch intermediary model>
if isinstance(self.translated_field, fields.related.ManyToManyField) and hasattr(
self.remote_field, "through"
Expand All @@ -187,7 +191,7 @@ def __init__(
or self.remote_field.model == self.model
):
self.remote_field.related_name = "%s_rel_+" % self.name
elif self.remote_field.is_hidden():
elif is_hidden(self.remote_field):
# Even if the backwards relation is disabled, django internally uses it, need to use a language scoped related_name
self.remote_field.related_name = "_%s_%s_+" % (
self.model.__name__.lower(),
Expand Down Expand Up @@ -218,7 +222,7 @@ def __init__(
if hasattr(self.remote_field.model._meta, "_related_objects_cache"):
del self.remote_field.model._meta._related_objects_cache

elif self.remote_field and not self.remote_field.is_hidden():
elif self.remote_field and not is_hidden(self.remote_field):
current = self.remote_field.get_accessor_name()
# Since fields cannot share the same rel object:
self.remote_field = copy.copy(self.remote_field)
Expand Down Expand Up @@ -481,17 +485,3 @@ def __set__(self, instance, value):
loc_field_name = build_localized_fieldname(self.field_name, get_language())
loc_attname = instance._meta.get_field(loc_field_name).get_attname()
setattr(instance, loc_attname, value)


class LanguageCacheSingleObjectDescriptor:
"""
A Mixin for RelatedObjectDescriptors which use current language in cache lookups.
"""

accessor = None # needs to be set on instance

def get_cache_name(self) -> str:
"""
Used in django > 2.x
"""
return build_localized_fieldname(self.accessor, get_language()) # type: ignore[arg-type]
2 changes: 2 additions & 0 deletions modeltranslation/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2680,6 +2680,8 @@ class OneToOneFieldModelAdmin(admin.TranslationAdmin):
fields = ["test_de", "test_en"]
for field in fields:
widget = ma.get_form(request).base_fields.get(field).widget
# Django 5.1 Adds this attr, ignore it
widget.attrs.pop("data-context", None)
assert {} == widget.attrs
assert "class" in widget.widget.attrs.keys()
assert "mt" in widget.widget.attrs["class"]
Expand Down
31 changes: 20 additions & 11 deletions modeltranslation/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from modeltranslation import settings as mt_settings
from modeltranslation.fields import (
NONE,
LanguageCacheSingleObjectDescriptor,
TranslatedManyToManyDescriptor,
TranslatedRelationIdDescriptor,
TranslationFieldDescriptor,
Expand All @@ -35,11 +34,16 @@
rewrite_lookup_key,
)
from modeltranslation.thread_context import auto_populate_mode
from modeltranslation.utils import build_localized_fieldname, parse_field
from modeltranslation.utils import (
build_localized_fieldname,
parse_field,
get_language,
)

# Re-export the decorator for convenience
from modeltranslation.decorators import register

from ._compat import is_hidden
from ._typing import _ListOrTuple

__all__ = [
Expand Down Expand Up @@ -458,16 +462,21 @@ def patch_related_object_descriptor_caching(ro_descriptor):
language-aware caching.
"""

class NewSingleObjectDescriptor(LanguageCacheSingleObjectDescriptor, ro_descriptor.__class__):
pass
class NewRelated(ro_descriptor.related.__class__):
def get_cache_name(self) -> str:
"""
Used in django > 2.x
"""
return self.cache_name

ro_descriptor.related.get_cache_name = partial(
NewSingleObjectDescriptor.get_cache_name,
ro_descriptor,
)
@property
def cache_name(self):
"""
Used in django >= 5.1
"""
return build_localized_fieldname(self.get_accessor_name(), get_language())

ro_descriptor.accessor = ro_descriptor.related.get_accessor_name()
ro_descriptor.__class__ = NewSingleObjectDescriptor
ro_descriptor.related.__class__ = NewRelated


class Translator:
Expand Down Expand Up @@ -599,7 +608,7 @@ def _register_single_model(self, model: type[Model], opts: TranslationOptions) -
setattr(model, field.get_attname(), desc)

# Set related field names on other model
if not field.remote_field.is_hidden():
if not is_hidden(field.remote_field):
other_opts = self._get_options_for_model(field.remote_field.model)
other_opts.related = True
other_opts.related_fields.append(field.related_query_name())
Expand Down
1 change: 0 additions & 1 deletion modeltranslation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from contextlib import contextmanager
from typing import Any, TypeVar
from collections.abc import Generator, Iterable, Iterator

from django.db import models
from django.utils.encoding import force_str
from django.utils.functional import lazy
Expand Down

0 comments on commit 7a60ca1

Please sign in to comment.