Skip to content

Commit 7a60ca1

Browse files
feat: Support django 5.1 (#756)
Refs #719
1 parent 22eef09 commit 7a60ca1

File tree

6 files changed

+62
-29
lines changed

6 files changed

+62
-29
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@ jobs:
4646
strategy:
4747
matrix:
4848
python: ["3.9", "3.10", "3.11", "3.12"]
49-
django: ["4.2", "5.0"]
49+
django: ["4.2", "5.0", "5.1"]
5050
database: ["sqlite", "postgres", "mysql"]
5151
exclude:
5252
- python: 3.9
5353
django: 5.0
54+
- python: 3.9
55+
django: 5.1
5456
env:
5557
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5658
DJANGO: ${{ matrix.django }}

modeltranslation/_compat.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import django
6+
7+
if TYPE_CHECKING:
8+
from django.db.models.fields.reverse_related import ForeignObjectRel
9+
10+
11+
def is_hidden(field: ForeignObjectRel) -> bool:
12+
return field.hidden
13+
14+
15+
def clear_ForeignObjectRel_caches(field: ForeignObjectRel):
16+
"""
17+
Django 5.1 Introduced caching for `accessor_name` props.
18+
19+
We need to clear this cache when creating Translated field.
20+
21+
https://github.com/django/django/commit/5e80390add100e0c7a1ac8e51739f94c5d706ea3#diff-e65b05ecbbe594164125af53550a43ef8a174f80811608012bc8e9e4ed575749
22+
"""
23+
caches = ("accessor_name",)
24+
for name in caches:
25+
field.__dict__.pop(name, None)
26+
27+
28+
if django.VERSION <= (5, 1):
29+
30+
def is_hidden(field: ForeignObjectRel) -> bool:
31+
return field.is_hidden()

modeltranslation/fields.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from modeltranslation.widgets import ClearableWidgetWrapper
2424

2525
from ._typing import Self
26+
from ._compat import is_hidden, clear_ForeignObjectRel_caches
2627

2728
SUPPORTED_FIELDS = (
2829
fields.CharField,
@@ -173,6 +174,9 @@ def __init__(
173174
# (will show up e.g. in the admin).
174175
self.verbose_name = build_localized_verbose_name(translated_field.verbose_name, language)
175176

177+
if self.remote_field:
178+
clear_ForeignObjectRel_caches(self.remote_field)
179+
176180
# M2M support - <rewrite related_name> <patch intermediary model>
177181
if isinstance(self.translated_field, fields.related.ManyToManyField) and hasattr(
178182
self.remote_field, "through"
@@ -187,7 +191,7 @@ def __init__(
187191
or self.remote_field.model == self.model
188192
):
189193
self.remote_field.related_name = "%s_rel_+" % self.name
190-
elif self.remote_field.is_hidden():
194+
elif is_hidden(self.remote_field):
191195
# Even if the backwards relation is disabled, django internally uses it, need to use a language scoped related_name
192196
self.remote_field.related_name = "_%s_%s_+" % (
193197
self.model.__name__.lower(),
@@ -218,7 +222,7 @@ def __init__(
218222
if hasattr(self.remote_field.model._meta, "_related_objects_cache"):
219223
del self.remote_field.model._meta._related_objects_cache
220224

221-
elif self.remote_field and not self.remote_field.is_hidden():
225+
elif self.remote_field and not is_hidden(self.remote_field):
222226
current = self.remote_field.get_accessor_name()
223227
# Since fields cannot share the same rel object:
224228
self.remote_field = copy.copy(self.remote_field)
@@ -481,17 +485,3 @@ def __set__(self, instance, value):
481485
loc_field_name = build_localized_fieldname(self.field_name, get_language())
482486
loc_attname = instance._meta.get_field(loc_field_name).get_attname()
483487
setattr(instance, loc_attname, value)
484-
485-
486-
class LanguageCacheSingleObjectDescriptor:
487-
"""
488-
A Mixin for RelatedObjectDescriptors which use current language in cache lookups.
489-
"""
490-
491-
accessor = None # needs to be set on instance
492-
493-
def get_cache_name(self) -> str:
494-
"""
495-
Used in django > 2.x
496-
"""
497-
return build_localized_fieldname(self.accessor, get_language()) # type: ignore[arg-type]

modeltranslation/tests/tests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2680,6 +2680,8 @@ class OneToOneFieldModelAdmin(admin.TranslationAdmin):
26802680
fields = ["test_de", "test_en"]
26812681
for field in fields:
26822682
widget = ma.get_form(request).base_fields.get(field).widget
2683+
# Django 5.1 Adds this attr, ignore it
2684+
widget.attrs.pop("data-context", None)
26832685
assert {} == widget.attrs
26842686
assert "class" in widget.widget.attrs.keys()
26852687
assert "mt" in widget.widget.attrs["class"]

modeltranslation/translator.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from modeltranslation import settings as mt_settings
2222
from modeltranslation.fields import (
2323
NONE,
24-
LanguageCacheSingleObjectDescriptor,
2524
TranslatedManyToManyDescriptor,
2625
TranslatedRelationIdDescriptor,
2726
TranslationFieldDescriptor,
@@ -35,11 +34,16 @@
3534
rewrite_lookup_key,
3635
)
3736
from modeltranslation.thread_context import auto_populate_mode
38-
from modeltranslation.utils import build_localized_fieldname, parse_field
37+
from modeltranslation.utils import (
38+
build_localized_fieldname,
39+
parse_field,
40+
get_language,
41+
)
3942

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

46+
from ._compat import is_hidden
4347
from ._typing import _ListOrTuple
4448

4549
__all__ = [
@@ -458,16 +462,21 @@ def patch_related_object_descriptor_caching(ro_descriptor):
458462
language-aware caching.
459463
"""
460464

461-
class NewSingleObjectDescriptor(LanguageCacheSingleObjectDescriptor, ro_descriptor.__class__):
462-
pass
465+
class NewRelated(ro_descriptor.related.__class__):
466+
def get_cache_name(self) -> str:
467+
"""
468+
Used in django > 2.x
469+
"""
470+
return self.cache_name
463471

464-
ro_descriptor.related.get_cache_name = partial(
465-
NewSingleObjectDescriptor.get_cache_name,
466-
ro_descriptor,
467-
)
472+
@property
473+
def cache_name(self):
474+
"""
475+
Used in django >= 5.1
476+
"""
477+
return build_localized_fieldname(self.get_accessor_name(), get_language())
468478

469-
ro_descriptor.accessor = ro_descriptor.related.get_accessor_name()
470-
ro_descriptor.__class__ = NewSingleObjectDescriptor
479+
ro_descriptor.related.__class__ = NewRelated
471480

472481

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

601610
# Set related field names on other model
602-
if not field.remote_field.is_hidden():
611+
if not is_hidden(field.remote_field):
603612
other_opts = self._get_options_for_model(field.remote_field.model)
604613
other_opts.related = True
605614
other_opts.related_fields.append(field.related_query_name())

modeltranslation/utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from contextlib import contextmanager
55
from typing import Any, TypeVar
66
from collections.abc import Generator, Iterable, Iterator
7-
87
from django.db import models
98
from django.utils.encoding import force_str
109
from django.utils.functional import lazy

0 commit comments

Comments
 (0)