diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6ef0872..6183264f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 }} diff --git a/modeltranslation/_compat.py b/modeltranslation/_compat.py new file mode 100644 index 00000000..419d6172 --- /dev/null +++ b/modeltranslation/_compat.py @@ -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() diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index b89d2bc6..f147c3bb 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -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, @@ -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 - if isinstance(self.translated_field, fields.related.ManyToManyField) and hasattr( self.remote_field, "through" @@ -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(), @@ -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) @@ -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] diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 23564f99..9a6fb630 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -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"] diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 5ab350e4..7456efa0 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -21,7 +21,6 @@ from modeltranslation import settings as mt_settings from modeltranslation.fields import ( NONE, - LanguageCacheSingleObjectDescriptor, TranslatedManyToManyDescriptor, TranslatedRelationIdDescriptor, TranslationFieldDescriptor, @@ -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__ = [ @@ -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: @@ -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()) diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index 43d3795b..d74cf36d 100644 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -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