mirror of
https://github.com/Hopiu/django-modeltranslation.git
synced 2026-03-16 22:10:31 +00:00
parent
2a9c823919
commit
a9e95e8c78
25 changed files with 628 additions and 285 deletions
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
|
|
@ -29,6 +29,13 @@ jobs:
|
|||
- name: Run linters
|
||||
run: |
|
||||
make lint
|
||||
- name: Install type checking dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install django==4.2.10 django-stubs==4.2.7 mypy==1.8.0
|
||||
- name: Run type checking
|
||||
run: |
|
||||
make typecheck
|
||||
- name: Test package install
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
*.py[cod]
|
||||
__pycache__
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
|
|
|||
3
Makefile
3
Makefile
|
|
@ -12,3 +12,6 @@ clean:
|
|||
lint:
|
||||
ruff check modeltranslation
|
||||
ruff format --check modeltranslation *.py
|
||||
|
||||
typecheck:
|
||||
mypy modeltranslation
|
||||
|
|
|
|||
16
modeltranslation/_typing.py
Normal file
16
modeltranslation/_typing.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Literal, TypeVar
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self, TypeAlias # noqa: F401
|
||||
else:
|
||||
from typing_extensions import Self, TypeAlias # noqa: F401
|
||||
|
||||
AutoPopulate: TypeAlias = "bool | Literal['all', 'default', 'required']"
|
||||
|
||||
_K = TypeVar("_K")
|
||||
|
||||
# See https://github.com/typeddjango/django-stubs/blob/082955/django-stubs/utils/datastructures.pyi#L12-L14
|
||||
_ListOrTuple: TypeAlias = "list[_K] | tuple[_K, ...]"
|
||||
|
|
@ -1,11 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any, Iterable, Sequence
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Field
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.options import BaseModelAdmin, InlineModelAdmin, flatten_fieldsets
|
||||
from django.contrib.contenttypes.admin import GenericStackedInline, GenericTabularInline
|
||||
from django.forms.models import BaseInlineFormSet
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from modeltranslation import settings as mt_settings
|
||||
from modeltranslation.fields import TranslationField
|
||||
from modeltranslation.translator import translator
|
||||
from modeltranslation.utils import (
|
||||
build_css_class,
|
||||
|
|
@ -16,18 +23,21 @@ from modeltranslation.utils import (
|
|||
unique,
|
||||
)
|
||||
from modeltranslation.widgets import ClearableWidgetWrapper
|
||||
from modeltranslation._typing import _ListOrTuple
|
||||
|
||||
|
||||
class TranslationBaseModelAdmin(BaseModelAdmin):
|
||||
_orig_was_required = {}
|
||||
_orig_was_required: dict[str, bool] = {}
|
||||
both_empty_values_fields = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.trans_opts = translator.get_options_for_model(self.model)
|
||||
self._patch_prepopulated_fields()
|
||||
|
||||
def _get_declared_fieldsets(self, request, obj=None):
|
||||
def _get_declared_fieldsets(
|
||||
self, request: HttpRequest, obj: Any | None = None
|
||||
) -> _ListOrTuple[tuple[str | None, dict[str, Any]]] | None:
|
||||
# Take custom modelform fields option into account
|
||||
if not self.fields and hasattr(self.form, "_meta") and self.form._meta.fields:
|
||||
self.fields = self.form._meta.fields
|
||||
|
|
@ -43,12 +53,16 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
return [(None, {"fields": self.replace_orig_field(self.get_fields(request, obj))})]
|
||||
return None
|
||||
|
||||
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
||||
def formfield_for_dbfield(
|
||||
self, db_field: Field, request: HttpRequest, **kwargs: Any
|
||||
) -> forms.Field:
|
||||
field = super().formfield_for_dbfield(db_field, request, **kwargs)
|
||||
self.patch_translation_field(db_field, field, request, **kwargs)
|
||||
return field
|
||||
self.patch_translation_field(db_field, field, request, **kwargs) # type: ignore[arg-type]
|
||||
return field # type: ignore[return-value]
|
||||
|
||||
def patch_translation_field(self, db_field, field, request, **kwargs):
|
||||
def patch_translation_field(
|
||||
self, db_field: Field, field: forms.Field, request: HttpRequest, **kwargs: Any
|
||||
) -> None:
|
||||
if db_field.name in self.trans_opts.fields:
|
||||
if field.required:
|
||||
field.required = False
|
||||
|
|
@ -67,7 +81,8 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
attrs = field.widget.attrs
|
||||
# if any widget attrs are defined on the form they should be copied
|
||||
try:
|
||||
field.widget = deepcopy(self.form._meta.widgets[orig_field.name]) # this is a class
|
||||
# this is a class:
|
||||
field.widget = deepcopy(self.form._meta.widgets[orig_field.name]) # type: ignore[index]
|
||||
if isinstance(field.widget, type): # if not initialized
|
||||
field.widget = field.widget(attrs) # initialize form widget with attrs
|
||||
except (AttributeError, TypeError, KeyError):
|
||||
|
|
@ -113,14 +128,14 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
field.widget = field.widget.widget
|
||||
self._get_widget_from_field(field).attrs["class"] = " ".join(css_classes)
|
||||
|
||||
def _get_widget_from_field(self, field):
|
||||
def _get_widget_from_field(self, field: forms.Field) -> Any:
|
||||
# retrieve "nested" widget in case of related field
|
||||
if isinstance(field.widget, admin.widgets.RelatedFieldWidgetWrapper):
|
||||
return field.widget.widget
|
||||
else:
|
||||
return field.widget
|
||||
|
||||
def _exclude_original_fields(self, exclude=None):
|
||||
def _exclude_original_fields(self, exclude: _ListOrTuple[str] | None = None) -> tuple[str, ...]:
|
||||
if exclude is None:
|
||||
exclude = tuple()
|
||||
if exclude:
|
||||
|
|
@ -128,7 +143,7 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
return exclude_new + tuple(self.trans_opts.fields.keys())
|
||||
return tuple(self.trans_opts.fields.keys())
|
||||
|
||||
def replace_orig_field(self, option):
|
||||
def replace_orig_field(self, option: Iterable[str | Sequence[str]]) -> _ListOrTuple[str]:
|
||||
"""
|
||||
Replaces each original field in `option` that is registered for
|
||||
translation by its translation fields.
|
||||
|
|
@ -158,16 +173,18 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
for opt in option:
|
||||
if opt in self.trans_opts.fields:
|
||||
index = option_new.index(opt)
|
||||
option_new[index : index + 1] = get_translation_fields(opt)
|
||||
option_new[index : index + 1] = get_translation_fields(opt) # type: ignore[arg-type]
|
||||
elif isinstance(opt, (tuple, list)) and (
|
||||
[o for o in opt if o in self.trans_opts.fields]
|
||||
):
|
||||
index = option_new.index(opt)
|
||||
option_new[index : index + 1] = self.replace_orig_field(opt)
|
||||
option = option_new
|
||||
return option
|
||||
return option # type: ignore[return-value]
|
||||
|
||||
def _patch_fieldsets(self, fieldsets):
|
||||
def _patch_fieldsets(
|
||||
self, fieldsets: _ListOrTuple[tuple[str | None, dict[str, Any]]]
|
||||
) -> _ListOrTuple[tuple[str | None, dict[str, Any]]]:
|
||||
if fieldsets:
|
||||
fieldsets_new = list(fieldsets)
|
||||
for name, dct in fieldsets:
|
||||
|
|
@ -176,18 +193,18 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
fieldsets = fieldsets_new
|
||||
return fieldsets
|
||||
|
||||
def _patch_prepopulated_fields(self):
|
||||
def localize(sources, lang):
|
||||
def _patch_prepopulated_fields(self) -> None:
|
||||
def localize(sources: Sequence[str], lang: str) -> tuple[str, ...]:
|
||||
"Append lang suffix (if applicable) to field list"
|
||||
|
||||
def append_lang(source):
|
||||
def append_lang(source: str) -> str:
|
||||
if source in self.trans_opts.fields:
|
||||
return build_localized_fieldname(source, lang)
|
||||
return source
|
||||
|
||||
return tuple(map(append_lang, sources))
|
||||
|
||||
prepopulated_fields = {}
|
||||
prepopulated_fields: dict[str, Sequence[str]] = {}
|
||||
for dest, sources in self.prepopulated_fields.items():
|
||||
if dest in self.trans_opts.fields:
|
||||
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
||||
|
|
@ -198,7 +215,9 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
prepopulated_fields[dest] = localize(sources, lang)
|
||||
self.prepopulated_fields = prepopulated_fields
|
||||
|
||||
def _get_form_or_formset(self, request, obj, **kwargs):
|
||||
def _get_form_or_formset(
|
||||
self, request: HttpRequest, obj: Any | None, **kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Generic code shared by get_form and get_formset.
|
||||
"""
|
||||
|
|
@ -220,14 +239,18 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
|
||||
return kwargs
|
||||
|
||||
def _get_fieldsets_pre_form_or_formset(self, request, obj=None):
|
||||
def _get_fieldsets_pre_form_or_formset(
|
||||
self, request: HttpRequest, obj: Any | None = None
|
||||
) -> _ListOrTuple[tuple[str | None, dict[str, Any]]] | None:
|
||||
"""
|
||||
Generic get_fieldsets code, shared by
|
||||
TranslationAdmin and TranslationInlineModelAdmin.
|
||||
"""
|
||||
return self._get_declared_fieldsets(request, obj)
|
||||
|
||||
def _get_fieldsets_post_form_or_formset(self, request, form, obj=None):
|
||||
def _get_fieldsets_post_form_or_formset(
|
||||
self, request: HttpRequest, form: type[forms.ModelForm], obj: Any | None = None
|
||||
) -> list:
|
||||
"""
|
||||
Generic get_fieldsets code, shared by
|
||||
TranslationAdmin and TranslationInlineModelAdmin.
|
||||
|
|
@ -236,7 +259,9 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
fields = list(base_fields) + list(self.get_readonly_fields(request, obj))
|
||||
return [(None, {"fields": self.replace_orig_field(fields)})]
|
||||
|
||||
def get_translation_field_excludes(self, exclude_languages=None):
|
||||
def get_translation_field_excludes(
|
||||
self, exclude_languages: list[str] | None = None
|
||||
) -> tuple[TranslationField, ...]:
|
||||
"""
|
||||
Returns a tuple of translation field names to exclude based on
|
||||
`exclude_languages` arg.
|
||||
|
|
@ -254,7 +279,9 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
exclude.append(tfield)
|
||||
return tuple(exclude)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
def get_readonly_fields(
|
||||
self, request: HttpRequest, obj: Any | None = None
|
||||
) -> _ListOrTuple[str]:
|
||||
"""
|
||||
Hook to specify custom readonly fields.
|
||||
"""
|
||||
|
|
@ -265,11 +292,11 @@ class TranslationAdmin(TranslationBaseModelAdmin, admin.ModelAdmin):
|
|||
# TODO: Consider addition of a setting which allows to override the fallback to True
|
||||
group_fieldsets = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._patch_list_editable()
|
||||
|
||||
def _patch_list_editable(self):
|
||||
def _patch_list_editable(self) -> None:
|
||||
if self.list_editable:
|
||||
editable_new = list(self.list_editable)
|
||||
display_new = list(self.list_display)
|
||||
|
|
@ -283,7 +310,7 @@ class TranslationAdmin(TranslationBaseModelAdmin, admin.ModelAdmin):
|
|||
self.list_editable = editable_new
|
||||
self.list_display = display_new
|
||||
|
||||
def _group_fieldsets(self, fieldsets):
|
||||
def _group_fieldsets(self, fieldsets: list) -> list:
|
||||
# Fieldsets are not grouped by default. The function is activated by
|
||||
# setting TranslationAdmin.group_fieldsets to True. If the admin class
|
||||
# already defines a fieldset, we leave it alone and assume the author
|
||||
|
|
@ -292,7 +319,7 @@ class TranslationAdmin(TranslationBaseModelAdmin, admin.ModelAdmin):
|
|||
flattened_fieldsets = flatten_fieldsets(fieldsets)
|
||||
|
||||
# Create a fieldset to group each translated field's localized fields
|
||||
fields = sorted(f for f in self.opts.get_fields() if f.concrete)
|
||||
fields = sorted(f for f in self.opts.get_fields() if f.concrete) # type: ignore[type-var]
|
||||
untranslated_fields = [
|
||||
f.name
|
||||
for f in fields
|
||||
|
|
@ -346,11 +373,15 @@ class TranslationAdmin(TranslationBaseModelAdmin, admin.ModelAdmin):
|
|||
|
||||
return fieldsets
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
def get_form(
|
||||
self, request: HttpRequest, obj: Any | None = None, **kwargs: Any
|
||||
) -> type[forms.ModelForm]:
|
||||
kwargs = self._get_form_or_formset(request, obj, **kwargs)
|
||||
return super().get_form(request, obj, **kwargs)
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
def get_fieldsets(
|
||||
self, request: HttpRequest, obj: Any | None = None
|
||||
) -> _ListOrTuple[tuple[str | None, dict[str, Any]]]:
|
||||
return self._get_fieldsets_pre_form_or_formset(request, obj) or self._group_fieldsets(
|
||||
self._get_fieldsets_post_form_or_formset(
|
||||
request, self.get_form(request, obj, fields=None), obj
|
||||
|
|
@ -359,11 +390,13 @@ class TranslationAdmin(TranslationBaseModelAdmin, admin.ModelAdmin):
|
|||
|
||||
|
||||
class TranslationInlineModelAdmin(TranslationBaseModelAdmin, InlineModelAdmin):
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
def get_formset(
|
||||
self, request: HttpRequest, obj: Any | None = None, **kwargs: Any
|
||||
) -> type[BaseInlineFormSet]:
|
||||
kwargs = self._get_form_or_formset(request, obj, **kwargs)
|
||||
return super().get_formset(request, obj, **kwargs)
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
def get_fieldsets(self, request: HttpRequest, obj: Any | None = None):
|
||||
# FIXME: If fieldsets are declared on an inline some kind of ghost
|
||||
# fieldset line with just the original model verbose_name of the model
|
||||
# is displayed above the new fieldsets.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class ModeltranslationConfig(AppConfig):
|
|||
name = "modeltranslation"
|
||||
verbose_name = "Modeltranslation"
|
||||
|
||||
def ready(self):
|
||||
def ready(self) -> None:
|
||||
from modeltranslation.models import handle_translation_registrations
|
||||
|
||||
handle_translation_registrations()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,18 @@
|
|||
def register(model_or_iterable, **options):
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, TypeVar
|
||||
|
||||
from django.db.models import Model
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from modeltranslation.translator import TranslationOptions
|
||||
|
||||
_TranslationOptionsTypeT = TypeVar("_TranslationOptionsTypeT", bound=type[TranslationOptions])
|
||||
|
||||
|
||||
def register(
|
||||
model_or_iterable: type[Model] | Iterable[type[Model]], **options: Any
|
||||
) -> Callable[[_TranslationOptionsTypeT], _TranslationOptionsTypeT]:
|
||||
"""
|
||||
Registers the given model(s) with the given translation options.
|
||||
|
||||
|
|
@ -14,7 +28,7 @@ def register(model_or_iterable, **options):
|
|||
"""
|
||||
from modeltranslation.translator import TranslationOptions, translator
|
||||
|
||||
def wrapper(opts_class):
|
||||
def wrapper(opts_class: _TranslationOptionsTypeT) -> _TranslationOptionsTypeT:
|
||||
if not issubclass(opts_class, TranslationOptions):
|
||||
raise ValueError("Wrapped class must subclass TranslationOptions.")
|
||||
translator.register(model_or_iterable, opts_class, **options)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import Iterable
|
||||
from typing import Any, Sequence, cast
|
||||
|
||||
from django import VERSION, forms
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import fields
|
||||
from django.db.models import Model, fields
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.translation import override
|
||||
|
|
@ -19,6 +21,8 @@ from modeltranslation.utils import (
|
|||
)
|
||||
from modeltranslation.widgets import ClearableWidgetWrapper
|
||||
|
||||
from ._typing import Self
|
||||
|
||||
SUPPORTED_FIELDS = (
|
||||
fields.CharField,
|
||||
# Above implies also CommaSeparatedIntegerField, EmailField, FilePathField, SlugField
|
||||
|
|
@ -57,7 +61,7 @@ class NONE:
|
|||
pass
|
||||
|
||||
|
||||
def create_translation_field(model, field_name, lang, empty_value):
|
||||
def create_translation_field(model: type[Model], field_name: str, lang: str, empty_value: Any):
|
||||
"""
|
||||
Translation field factory. Returns a ``TranslationField`` based on a
|
||||
fieldname and a language.
|
||||
|
|
@ -72,7 +76,7 @@ def create_translation_field(model, field_name, lang, empty_value):
|
|||
"""
|
||||
if empty_value not in ("", "both", None, NONE):
|
||||
raise ImproperlyConfigured("%s is not a valid empty_value." % empty_value)
|
||||
field = model._meta.get_field(field_name)
|
||||
field = cast(fields.Field, model._meta.get_field(field_name))
|
||||
cls_name = field.__class__.__name__
|
||||
if not (isinstance(field, SUPPORTED_FIELDS) or cls_name in mt_settings.CUSTOM_FIELDS):
|
||||
raise ImproperlyConfigured("%s is not supported by modeltranslation." % cls_name)
|
||||
|
|
@ -80,8 +84,8 @@ def create_translation_field(model, field_name, lang, empty_value):
|
|||
return translation_class(translated_field=field, language=lang, empty_value=empty_value)
|
||||
|
||||
|
||||
def field_factory(baseclass):
|
||||
class TranslationFieldSpecific(TranslationField, baseclass):
|
||||
def field_factory(baseclass: type[fields.Field]) -> type[TranslationField]:
|
||||
class TranslationFieldSpecific(TranslationField, baseclass): # type: ignore[valid-type, misc]
|
||||
pass
|
||||
|
||||
# Reflect baseclass name of returned subclass
|
||||
|
|
@ -109,7 +113,14 @@ class TranslationField:
|
|||
that needs to be specified when the field is created.
|
||||
"""
|
||||
|
||||
def __init__(self, translated_field, language, empty_value, *args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
translated_field: fields.Field,
|
||||
language: str,
|
||||
empty_value: Any,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
from modeltranslation.translator import translator
|
||||
|
||||
# Update the dict of this field with the content of the original one
|
||||
|
|
@ -134,7 +145,7 @@ class TranslationField:
|
|||
trans_opts = translator.get_options_for_model(self.model)
|
||||
if trans_opts.required_languages:
|
||||
required_languages = trans_opts.required_languages
|
||||
if isinstance(trans_opts.required_languages, (tuple, list)):
|
||||
if isinstance(required_languages, (tuple, list)):
|
||||
# All fields
|
||||
if self.language in required_languages:
|
||||
# self.null = False
|
||||
|
|
@ -246,20 +257,20 @@ class TranslationField:
|
|||
# original field and fields didn't get added to sets.
|
||||
# So here we override __eq__ and __hash__ to fix the issue while retaining fine with
|
||||
# http://docs.python.org/2.7/reference/datamodel.html#object.__hash__
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, fields.Field):
|
||||
return self.creation_counter == other.creation_counter and self.language == getattr(
|
||||
other, "language", None
|
||||
)
|
||||
return super().__eq__(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.creation_counter, self.language))
|
||||
|
||||
def get_default(self):
|
||||
def get_default(self) -> Any:
|
||||
with override(self.language):
|
||||
default = super().get_default()
|
||||
# we must *force evaluation* at this point, otherwise the lazy translatable
|
||||
|
|
@ -272,7 +283,7 @@ class TranslationField:
|
|||
default = force_str(default, strings_only=True)
|
||||
return default
|
||||
|
||||
def formfield(self, *args, **kwargs):
|
||||
def formfield(self, *args: Any, **kwargs: Any) -> forms.Field:
|
||||
"""
|
||||
Returns proper formfield, according to empty_values setting
|
||||
(only for ``forms.CharField`` subclasses).
|
||||
|
|
@ -317,7 +328,7 @@ class TranslationField:
|
|||
formfield.widget = ClearableWidgetWrapper(formfield.widget)
|
||||
return formfield
|
||||
|
||||
def save_form_data(self, instance, data, check=True):
|
||||
def save_form_data(self, instance: Model, data: Any, check: bool = True) -> None:
|
||||
# Allow 3rd-party apps forms to be saved using only translated field name.
|
||||
# When translated field (e.g. 'name') is specified and translation field (e.g. 'name_en')
|
||||
# not, we assume that form was saved without knowledge of modeltranslation and we make
|
||||
|
|
@ -334,7 +345,7 @@ class TranslationField:
|
|||
else:
|
||||
super().save_form_data(instance, data)
|
||||
|
||||
def deconstruct(self):
|
||||
def deconstruct(self) -> tuple[str, str, Sequence[Any], dict[str, Any]]:
|
||||
name, path, args, kwargs = self.translated_field.deconstruct()
|
||||
if self.null is True:
|
||||
kwargs.update({"null": True})
|
||||
|
|
@ -342,7 +353,7 @@ class TranslationField:
|
|||
kwargs["db_column"] = self.db_column
|
||||
return self.name, path, args, kwargs
|
||||
|
||||
def clone(self):
|
||||
def clone(self) -> Self:
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
name, path, args, kwargs = self.deconstruct()
|
||||
|
|
@ -356,8 +367,12 @@ class TranslationFieldDescriptor:
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, field, fallback_languages=None, fallback_value=NONE, fallback_undefined=NONE
|
||||
):
|
||||
self,
|
||||
field: fields.Field,
|
||||
fallback_languages: dict[str, tuple[str, ...]] | None = None,
|
||||
fallback_value: Any = NONE,
|
||||
fallback_undefined: Any = NONE,
|
||||
) -> None:
|
||||
"""
|
||||
Stores fallback options and the original field, so we know it's name
|
||||
and default.
|
||||
|
|
@ -434,7 +449,9 @@ class TranslatedRelationIdDescriptor:
|
|||
ForeignKey field.
|
||||
"""
|
||||
|
||||
def __init__(self, field_name: str, fallback_languages: Iterable[str]):
|
||||
def __init__(
|
||||
self, field_name: str, fallback_languages: dict[str, tuple[str, ...]] | None
|
||||
) -> None:
|
||||
self.field_name = field_name # The name of the original field (excluding '_id')
|
||||
self.fallback_languages = fallback_languages
|
||||
|
||||
|
|
@ -466,7 +483,9 @@ class TranslatedManyToManyDescriptor:
|
|||
A descriptor used to return correct related manager without language fallbacks.
|
||||
"""
|
||||
|
||||
def __init__(self, field_name, fallback_languages):
|
||||
def __init__(
|
||||
self, field_name: str, fallback_languages: dict[str, tuple[str, ...]] | None
|
||||
) -> None:
|
||||
self.field_name = field_name # The name of the original field
|
||||
self.fallback_languages = fallback_languages
|
||||
|
||||
|
|
@ -490,16 +509,16 @@ class LanguageCacheSingleObjectDescriptor:
|
|||
accessor = None # needs to be set on instance
|
||||
|
||||
@property
|
||||
def cache_name(self):
|
||||
def cache_name(self) -> str:
|
||||
"""
|
||||
Used in django 1.x
|
||||
"""
|
||||
lang = get_language()
|
||||
cache = build_localized_fieldname(self.accessor, lang)
|
||||
cache = build_localized_fieldname(self.accessor, lang) # type: ignore[arg-type]
|
||||
return "_%s_cache" % cache
|
||||
|
||||
def get_cache_name(self):
|
||||
def get_cache_name(self) -> str:
|
||||
"""
|
||||
Used in django > 2.x
|
||||
"""
|
||||
return build_localized_fieldname(self.accessor, get_language())
|
||||
return build_localized_fieldname(self.accessor, get_language()) # type: ignore[arg-type]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
|
||||
|
|
@ -5,7 +9,7 @@ from modeltranslation.fields import TranslationField
|
|||
|
||||
|
||||
class TranslationModelForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
for f in self._meta.model._meta.fields:
|
||||
if f.name in self.fields and isinstance(f, TranslationField):
|
||||
|
|
@ -17,7 +21,7 @@ class NullCharField(forms.CharField):
|
|||
CharField subclass that returns ``None`` when ``CharField`` would return empty string.
|
||||
"""
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: Any | None) -> str | None:
|
||||
if value in validators.EMPTY_VALUES:
|
||||
return None
|
||||
return super().to_python(value)
|
||||
|
|
@ -29,7 +33,7 @@ class NullableField(forms.Field):
|
|||
the empty string with ``CharField`` and its derivatives).
|
||||
"""
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: Any | None) -> Any | None:
|
||||
if value is None:
|
||||
return value
|
||||
return super().to_python(value)
|
||||
|
|
|
|||
|
|
@ -1,32 +1,50 @@
|
|||
import argparse
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from argparse import Action, Namespace
|
||||
|
||||
from django.core.management.base import CommandParser
|
||||
from django.core.management.commands.loaddata import Command as LoadDataCommand
|
||||
|
||||
# Because this command is used (instead of default loaddata), then settings have been imported
|
||||
# and we can safely import MT modules
|
||||
from modeltranslation import settings as mt_settings
|
||||
from modeltranslation.utils import auto_populate
|
||||
from modeltranslation._typing import AutoPopulate
|
||||
|
||||
ALLOWED = (None, False, "all", "default", "required")
|
||||
ALLOWED_FOR_PRINT = ", ".join(str(i) for i in (0,) + ALLOWED[1:]) # For pretty-printing
|
||||
|
||||
|
||||
def check_mode(option, opt_str, value, parser, namespace=None):
|
||||
def check_mode(
|
||||
option: Command.CheckAction,
|
||||
opt_str: str | None,
|
||||
value: str,
|
||||
parser: CommandParser,
|
||||
namespace: Namespace | None = None,
|
||||
) -> None:
|
||||
if value == "0" or value.lower() == "false":
|
||||
value = False
|
||||
value = False # type: ignore[assignment]
|
||||
if value not in ALLOWED:
|
||||
raise ValueError("%s option can be only one of: %s" % (opt_str, ALLOWED_FOR_PRINT))
|
||||
setattr(namespace or parser.values, option.dest, value)
|
||||
setattr(namespace or parser.values, option.dest, value) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class Command(LoadDataCommand):
|
||||
leave_locale_alone = mt_settings.LOADDATA_RETAIN_LOCALE # Django 1.6
|
||||
|
||||
class CheckAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, value, option_string=None):
|
||||
class CheckAction(Action):
|
||||
def __call__(
|
||||
self,
|
||||
parser: CommandParser, # type: ignore[override]
|
||||
namespace: Namespace,
|
||||
value: str, # type: ignore[override]
|
||||
option_string: str | None = None,
|
||||
) -> None:
|
||||
check_mode(self, option_string, value, parser, namespace)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--populate",
|
||||
|
|
@ -40,8 +58,8 @@ class Command(LoadDataCommand):
|
|||
),
|
||||
)
|
||||
|
||||
def handle(self, *fixture_labels, **options):
|
||||
mode = options.get("populate")
|
||||
def handle(self, *fixture_labels: Any, **options: Any) -> str | None:
|
||||
mode: AutoPopulate | None = options.get("populate")
|
||||
if mode is not None:
|
||||
with auto_populate(mode):
|
||||
return super().handle(*fixture_labels, **options)
|
||||
|
|
|
|||
|
|
@ -8,17 +8,22 @@ You will need to execute this command in two cases:
|
|||
|
||||
Credits: Heavily inspired by django-transmeta's sync_transmeta_db command.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterator, cast
|
||||
|
||||
from django import VERSION
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import BaseCommand, CommandParser
|
||||
from django.core.management.color import no_style
|
||||
from django.db import connection
|
||||
from django.db.models import Model, Field
|
||||
|
||||
from modeltranslation.settings import AVAILABLE_LANGUAGES
|
||||
from modeltranslation.translator import translator
|
||||
from modeltranslation.utils import build_localized_fieldname
|
||||
|
||||
|
||||
def ask_for_confirmation(sql_sentences, model_full_name, interactive):
|
||||
def ask_for_confirmation(sql_sentences: list[str], model_full_name: str, interactive: bool) -> bool:
|
||||
print('\nSQL to synchronize "%s" schema:' % model_full_name)
|
||||
for sentence in sql_sentences:
|
||||
print(" %s" % sentence)
|
||||
|
|
@ -38,7 +43,7 @@ def ask_for_confirmation(sql_sentences, model_full_name, interactive):
|
|||
return False
|
||||
|
||||
|
||||
def print_missing_langs(missing_langs, field_name, model_name):
|
||||
def print_missing_langs(missing_langs: list[str], field_name: str, model_name: str) -> None:
|
||||
print(
|
||||
'Missing languages in "%s" field from "%s" model: %s'
|
||||
% (field_name, model_name, ", ".join(missing_langs))
|
||||
|
|
@ -55,7 +60,7 @@ class Command(BaseCommand):
|
|||
if VERSION < (1, 8):
|
||||
from optparse import make_option
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
option_list = BaseCommand.option_list + ( # type: ignore
|
||||
make_option(
|
||||
"--noinput",
|
||||
action="store_false",
|
||||
|
|
@ -66,7 +71,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
else:
|
||||
|
||||
def add_arguments(self, parser):
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
(
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
|
|
@ -77,7 +82,7 @@ class Command(BaseCommand):
|
|||
),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
"""
|
||||
Command execution.
|
||||
"""
|
||||
|
|
@ -120,14 +125,14 @@ class Command(BaseCommand):
|
|||
if not found_missing_fields:
|
||||
print("No new translatable fields detected")
|
||||
|
||||
def get_table_fields(self, db_table):
|
||||
def get_table_fields(self, db_table: str) -> list[str]:
|
||||
"""
|
||||
Gets table fields from schema.
|
||||
"""
|
||||
db_table_desc = self.introspection.get_table_description(self.cursor, db_table)
|
||||
return [t[0] for t in db_table_desc]
|
||||
|
||||
def get_missing_languages(self, field_name, db_table):
|
||||
def get_missing_languages(self, field_name: str, db_table: str) -> Iterator[str]:
|
||||
"""
|
||||
Gets only missing fields.
|
||||
"""
|
||||
|
|
@ -136,19 +141,21 @@ class Command(BaseCommand):
|
|||
if build_localized_fieldname(field_name, lang_code) not in db_table_fields:
|
||||
yield lang_code
|
||||
|
||||
def get_sync_sql(self, field_name, missing_langs, model):
|
||||
def get_sync_sql(
|
||||
self, field_name: str, missing_langs: list[str], model: type[Model]
|
||||
) -> list[str]:
|
||||
"""
|
||||
Returns SQL needed for sync schema for a new translatable field.
|
||||
"""
|
||||
qn = connection.ops.quote_name
|
||||
style = no_style()
|
||||
sql_output = []
|
||||
sql_output: list[str] = []
|
||||
db_table = model._meta.db_table
|
||||
for lang in missing_langs:
|
||||
new_field = build_localized_fieldname(field_name, lang)
|
||||
f = model._meta.get_field(new_field)
|
||||
f = cast(Field, model._meta.get_field(new_field))
|
||||
col_type = f.db_type(connection=connection)
|
||||
field_sql = [style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type)]
|
||||
field_sql = [style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type)] # type: ignore[arg-type]
|
||||
# column creation
|
||||
stmt = "ALTER TABLE %s ADD COLUMN %s" % (qn(db_table), " ".join(field_sql))
|
||||
if not f.null:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
from typing import Any
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandParser, CommandError
|
||||
from django.db.models import F, ManyToManyField, Q
|
||||
|
||||
from modeltranslation.settings import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE
|
||||
|
|
@ -14,7 +16,7 @@ class Command(BaseCommand):
|
|||
" values from original fields (in all translated models)."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
parser.add_argument(
|
||||
"app_label",
|
||||
nargs="?",
|
||||
|
|
@ -34,8 +36,8 @@ class Command(BaseCommand):
|
|||
),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbosity = options["verbosity"]
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
verbosity: int = options["verbosity"]
|
||||
if verbosity > 0:
|
||||
self.stdout.write("Using default language: %s" % DEFAULT_LANGUAGE)
|
||||
|
||||
|
|
@ -94,9 +96,9 @@ class Command(BaseCommand):
|
|||
for inst in getattr(model, field_name).through.objects.all()
|
||||
)
|
||||
continue
|
||||
if field.empty_strings_allowed:
|
||||
if field.empty_strings_allowed: # type: ignore[union-attr]
|
||||
q |= Q(**{def_lang_fieldname: ""})
|
||||
|
||||
model._default_manager.filter(q).rewrite(False).order_by().update(
|
||||
model._default_manager.filter(q).rewrite(False).order_by().update( # type: ignore[attr-defined]
|
||||
**{def_lang_fieldname: F(field_name)}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ django-linguo by Zach Mathew
|
|||
|
||||
https://github.com/zmathew/django-linguo
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from functools import reduce
|
||||
from typing import List, Tuple, Type, Any, Optional
|
||||
from typing import Any, Container, Iterator, Literal, Sequence, TypeVar, cast, overload
|
||||
|
||||
from django import VERSION
|
||||
from django.contrib.admin.utils import get_model_from_relation
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db import models
|
||||
from django.db.backends.utils import CursorWrapper
|
||||
from django.db.models import Field, Model
|
||||
from django.db.models.expressions import Col
|
||||
from django.db.models.lookups import Lookup
|
||||
|
|
@ -27,12 +30,13 @@ from modeltranslation.utils import (
|
|||
get_language,
|
||||
resolution_order,
|
||||
)
|
||||
from modeltranslation._typing import Self, AutoPopulate
|
||||
|
||||
_C2F_CACHE = {}
|
||||
_F2TM_CACHE = {}
|
||||
_C2F_CACHE: dict[tuple[type[Model], str], Field] = {}
|
||||
_F2TM_CACHE: dict[type[Model], dict[str, type[Model]]] = {}
|
||||
|
||||
|
||||
def get_translatable_fields_for_model(model):
|
||||
def get_translatable_fields_for_model(model: type[Model]) -> list[str] | None:
|
||||
from modeltranslation.translator import NotRegistered, translator
|
||||
|
||||
try:
|
||||
|
|
@ -41,7 +45,7 @@ def get_translatable_fields_for_model(model):
|
|||
return None
|
||||
|
||||
|
||||
def rewrite_lookup_key(model, lookup_key):
|
||||
def rewrite_lookup_key(model: type[Model], lookup_key: str) -> str:
|
||||
try:
|
||||
pieces = lookup_key.split("__", 1)
|
||||
original_key = pieces[0]
|
||||
|
|
@ -66,38 +70,38 @@ def rewrite_lookup_key(model, lookup_key):
|
|||
return lookup_key
|
||||
|
||||
|
||||
def append_fallback(model, fields):
|
||||
def append_fallback(model: type[Model], fields: Sequence[str]) -> tuple[set[str], set[str]]:
|
||||
"""
|
||||
If translated field is encountered, add also all its fallback fields.
|
||||
Returns tuple: (set_of_new_fields_to_use, set_of_translated_field_names)
|
||||
"""
|
||||
fields = set(fields)
|
||||
trans = set()
|
||||
fields_set = set(fields)
|
||||
trans: set[str] = set()
|
||||
from modeltranslation.translator import translator
|
||||
|
||||
opts = translator.get_options_for_model(model)
|
||||
for key, _ in opts.fields.items():
|
||||
if key in fields:
|
||||
if key in fields_set:
|
||||
langs = resolution_order(get_language(), getattr(model, key).fallback_languages)
|
||||
fields = fields.union(build_localized_fieldname(key, lang) for lang in langs)
|
||||
fields.remove(key)
|
||||
fields_set = fields_set.union(build_localized_fieldname(key, lang) for lang in langs)
|
||||
fields_set.remove(key)
|
||||
trans.add(key)
|
||||
return fields, trans
|
||||
return fields_set, trans
|
||||
|
||||
|
||||
def append_translated(model, fields):
|
||||
def append_translated(model: type[Model], fields: Sequence[str]) -> set[str]:
|
||||
"If translated field is encountered, add also all its translation fields."
|
||||
fields = set(fields)
|
||||
fields_set = set(fields)
|
||||
from modeltranslation.translator import translator
|
||||
|
||||
opts = translator.get_options_for_model(model)
|
||||
for key, translated in opts.fields.items():
|
||||
if key in fields:
|
||||
fields = fields.union(f.name for f in translated)
|
||||
return fields
|
||||
if key in fields_set:
|
||||
fields_set = fields_set.union(f.name for f in translated)
|
||||
return fields_set
|
||||
|
||||
|
||||
def append_lookup_key(model, lookup_key):
|
||||
def append_lookup_key(model: type[Model], lookup_key: str) -> set[str]:
|
||||
"Transform spanned__lookup__key into all possible translation versions, on all levels"
|
||||
pieces = lookup_key.split("__", 1)
|
||||
|
||||
|
|
@ -115,19 +119,19 @@ def append_lookup_key(model, lookup_key):
|
|||
return fields
|
||||
|
||||
|
||||
def append_lookup_keys(model, fields):
|
||||
def append_lookup_keys(model: type[Model], fields: Sequence[str]) -> set[str]:
|
||||
new_fields = []
|
||||
for field in fields:
|
||||
try:
|
||||
new_field = append_lookup_key(model, field)
|
||||
new_field: Container[str] = append_lookup_key(model, field)
|
||||
except AttributeError:
|
||||
new_field = (field,)
|
||||
new_fields.append(new_field)
|
||||
|
||||
return reduce(set.union, new_fields, set())
|
||||
return reduce(set.union, new_fields, set()) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def rewrite_order_lookup_key(model, lookup_key):
|
||||
def rewrite_order_lookup_key(model: type[Model], lookup_key: str) -> str:
|
||||
try:
|
||||
if lookup_key.startswith("-"):
|
||||
return "-" + rewrite_lookup_key(model, lookup_key[1:])
|
||||
|
|
@ -137,33 +141,33 @@ def rewrite_order_lookup_key(model, lookup_key):
|
|||
return lookup_key
|
||||
|
||||
|
||||
def get_fields_to_translatable_models(model):
|
||||
def get_fields_to_translatable_models(model: type[Model]) -> dict[str, type[Model]]:
|
||||
if model in _F2TM_CACHE:
|
||||
return _F2TM_CACHE[model]
|
||||
|
||||
results = []
|
||||
results: list[tuple[str, type[Model]]] = []
|
||||
for f in model._meta.get_fields():
|
||||
if f.is_relation and f.related_model:
|
||||
# The new get_field() will find GenericForeignKey relations.
|
||||
# In that case the 'related_model' attribute is set to None
|
||||
# so it is necessary to check for this value before trying to
|
||||
# get translatable fields.
|
||||
related_model = get_model_from_relation(f)
|
||||
related_model = get_model_from_relation(f) # type: ignore[arg-type]
|
||||
if get_translatable_fields_for_model(related_model) is not None:
|
||||
results.append((f.name, related_model))
|
||||
_F2TM_CACHE[model] = dict(results)
|
||||
return _F2TM_CACHE[model]
|
||||
|
||||
|
||||
def get_field_by_colum_name(model, col):
|
||||
def get_field_by_colum_name(model: type[Model], col: str) -> Field:
|
||||
# First, try field with the column name
|
||||
try:
|
||||
field = model._meta.get_field(col)
|
||||
field = cast(Field, model._meta.get_field(col))
|
||||
if field.column == col:
|
||||
return field
|
||||
except FieldDoesNotExist:
|
||||
pass
|
||||
field = _C2F_CACHE.get((model, col), None)
|
||||
field = _C2F_CACHE.get((model, col), None) # type: ignore[arg-type]
|
||||
if field:
|
||||
return field
|
||||
# D'oh, need to search through all of them.
|
||||
|
|
@ -174,12 +178,15 @@ def get_field_by_colum_name(model, col):
|
|||
assert False, "No field found for column %s" % col
|
||||
|
||||
|
||||
class MultilingualQuerySet(QuerySet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
_T = TypeVar("_T", bound=Model, covariant=True)
|
||||
|
||||
|
||||
class MultilingualQuerySet(QuerySet[_T]):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._post_init()
|
||||
|
||||
def _post_init(self):
|
||||
def _post_init(self) -> None:
|
||||
self._rewrite = True
|
||||
self._populate = None
|
||||
if self.model and self.query.default_ordering and (not self.query.order_by):
|
||||
|
|
@ -194,10 +201,10 @@ class MultilingualQuerySet(QuerySet):
|
|||
def __reduce__(self):
|
||||
return multilingual_queryset_factory, (self.__class__.__bases__[0],), self.__getstate__()
|
||||
|
||||
def _clone(self):
|
||||
def _clone(self) -> Self:
|
||||
return self.__clone()
|
||||
|
||||
def __clone(self, **kwargs):
|
||||
def __clone(self, **kwargs: Any) -> Self:
|
||||
# This method is private, so outside code can use default _clone without `kwargs`,
|
||||
# and we're here can use private version with `kwargs`.
|
||||
# Refs: https://github.com/deschler/django-modeltranslation/issues/483
|
||||
|
|
@ -211,16 +218,16 @@ class MultilingualQuerySet(QuerySet):
|
|||
cloned.__dict__.update(kwargs)
|
||||
return cloned
|
||||
|
||||
def rewrite(self, mode=True):
|
||||
def rewrite(self, mode: bool = True) -> Self:
|
||||
return self.__clone(_rewrite=mode)
|
||||
|
||||
def populate(self, mode="all"):
|
||||
def populate(self, mode: AutoPopulate = "all") -> Self:
|
||||
"""
|
||||
Overrides the translation fields population mode for this query set.
|
||||
"""
|
||||
return self.__clone(_populate=mode)
|
||||
|
||||
def _rewrite_applied_operations(self):
|
||||
def _rewrite_applied_operations(self) -> None:
|
||||
"""
|
||||
Rewrite fields in already applied filters/ordering.
|
||||
Useful when converting any QuerySet into MultilingualQuerySet.
|
||||
|
|
@ -230,14 +237,14 @@ class MultilingualQuerySet(QuerySet):
|
|||
self._rewrite_select_related()
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def select_related(self, *fields, **kwargs):
|
||||
def select_related(self, *fields: Any, **kwargs: Any) -> Self:
|
||||
if not self._rewrite:
|
||||
return super().select_related(*fields, **kwargs)
|
||||
# TO CONSIDER: whether this should rewrite only current language, or all languages?
|
||||
# fk -> [fk, fk_en] (with en=active) VS fk -> [fk, fk_en, fk_de, fk_fr ...] (for all langs)
|
||||
|
||||
# new_args = append_lookup_keys(self.model, fields)
|
||||
new_args = []
|
||||
new_args: list[str | None] = []
|
||||
for key in fields:
|
||||
if key is None:
|
||||
new_args.append(None)
|
||||
|
|
@ -246,7 +253,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
return super().select_related(*new_args, **kwargs)
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def _rewrite_col(self, col):
|
||||
def _rewrite_col(self, col: Col) -> None:
|
||||
"""Django >= 1.7 column name rewriting"""
|
||||
if isinstance(col, Col):
|
||||
new_name = rewrite_lookup_key(self.model, col.target.name)
|
||||
|
|
@ -260,7 +267,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
elif hasattr(col, "lhs"):
|
||||
self._rewrite_col(col.lhs)
|
||||
|
||||
def _rewrite_where(self, q):
|
||||
def _rewrite_where(self, q: Lookup | Node) -> None:
|
||||
"""
|
||||
Rewrite field names inside WHERE tree.
|
||||
"""
|
||||
|
|
@ -268,14 +275,14 @@ class MultilingualQuerySet(QuerySet):
|
|||
self._rewrite_col(q.lhs)
|
||||
if isinstance(q, Node):
|
||||
for child in q.children:
|
||||
self._rewrite_where(child)
|
||||
self._rewrite_where(child) # type: ignore[arg-type]
|
||||
|
||||
def _rewrite_order(self):
|
||||
def _rewrite_order(self) -> None:
|
||||
self.query.order_by = [
|
||||
rewrite_order_lookup_key(self.model, field_name) for field_name in self.query.order_by
|
||||
]
|
||||
|
||||
def _rewrite_select_related(self):
|
||||
def _rewrite_select_related(self) -> None:
|
||||
if isinstance(self.query.select_related, dict):
|
||||
new = {}
|
||||
for field_name, value in self.query.select_related.items():
|
||||
|
|
@ -283,16 +290,16 @@ class MultilingualQuerySet(QuerySet):
|
|||
self.query.select_related = new
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def _rewrite_q(self, q):
|
||||
def _rewrite_q(self, q: Node | tuple[str, Any]) -> Any:
|
||||
"""Rewrite field names inside Q call."""
|
||||
if isinstance(q, tuple) and len(q) == 2:
|
||||
return rewrite_lookup_key(self.model, q[0]), q[1]
|
||||
if isinstance(q, Node):
|
||||
q.children = list(map(self._rewrite_q, q.children))
|
||||
q.children = list(map(self._rewrite_q, q.children)) # type: ignore[arg-type]
|
||||
return q
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def _rewrite_f(self, q):
|
||||
def _rewrite_f(self, q: models.F | Node) -> models.F | Node:
|
||||
"""
|
||||
Rewrite field names inside F call.
|
||||
"""
|
||||
|
|
@ -300,7 +307,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
q.name = rewrite_lookup_key(self.model, q.name)
|
||||
return q
|
||||
if isinstance(q, Node):
|
||||
q.children = list(map(self._rewrite_f, q.children))
|
||||
q.children = list(map(self._rewrite_f, q.children)) # type: ignore[arg-type]
|
||||
# Django >= 1.8
|
||||
if hasattr(q, "lhs"):
|
||||
q.lhs = self._rewrite_f(q.lhs)
|
||||
|
|
@ -308,7 +315,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
q.rhs = self._rewrite_f(q.rhs)
|
||||
return q
|
||||
|
||||
def _rewrite_filter_or_exclude(self, args, kwargs):
|
||||
def _rewrite_filter_or_exclude(self, args: Any, kwargs: Any) -> tuple[Any, Any]:
|
||||
if not self._rewrite:
|
||||
return args, kwargs
|
||||
args = tuple(map(self._rewrite_q, args))
|
||||
|
|
@ -320,17 +327,17 @@ class MultilingualQuerySet(QuerySet):
|
|||
|
||||
if VERSION >= (3, 2):
|
||||
|
||||
def _filter_or_exclude(self, negate, args, kwargs):
|
||||
def _filter_or_exclude(self, negate: bool, args: Any, kwargs: Any) -> Self:
|
||||
args, kwargs = self._rewrite_filter_or_exclude(args, kwargs)
|
||||
return super()._filter_or_exclude(negate, args, kwargs)
|
||||
|
||||
else:
|
||||
|
||||
def _filter_or_exclude(self, negate, *args, **kwargs):
|
||||
def _filter_or_exclude(self, negate: bool, *args: Any, **kwargs: Any) -> Self:
|
||||
args, kwargs = self._rewrite_filter_or_exclude(args, kwargs)
|
||||
return super()._filter_or_exclude(negate, *args, **kwargs)
|
||||
|
||||
def _get_original_fields(self):
|
||||
def _get_original_fields(self) -> list[str]:
|
||||
source = (
|
||||
self.model._meta.concrete_fields
|
||||
if hasattr(self.model._meta, "concrete_fields")
|
||||
|
|
@ -338,7 +345,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
)
|
||||
return [f.attname for f in source if not isinstance(f, TranslationField)]
|
||||
|
||||
def order_by(self, *field_names):
|
||||
def order_by(self, *field_names: Any) -> Self:
|
||||
"""
|
||||
Change translatable field names in an ``order_by`` argument
|
||||
to translation fields for the current language.
|
||||
|
|
@ -350,7 +357,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
new_args.append(rewrite_order_lookup_key(self.model, key))
|
||||
return super().order_by(*new_args)
|
||||
|
||||
def distinct(self, *field_names):
|
||||
def distinct(self, *field_names: Any) -> Self:
|
||||
"""
|
||||
Change translatable field names in an ``distinct`` argument
|
||||
to translation fields for the current language.
|
||||
|
|
@ -362,7 +369,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
new_args.append(rewrite_order_lookup_key(self.model, key))
|
||||
return super().distinct(*new_args)
|
||||
|
||||
def update(self, **kwargs):
|
||||
def update(self, **kwargs: Any) -> int:
|
||||
if not self._rewrite:
|
||||
return super().update(**kwargs)
|
||||
for key, val in list(kwargs.items()):
|
||||
|
|
@ -373,7 +380,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
|
||||
update.alters_data = True
|
||||
|
||||
def _update(self, values: List[Tuple[Field, Optional[Type[Model]], Any]]):
|
||||
def _update(self, values: list[tuple[Field, type[Model] | None, Any]]) -> CursorWrapper:
|
||||
"""
|
||||
This method is called in .save() method to update an existing record.
|
||||
Here we force to update translation fields as well if the original
|
||||
|
|
@ -384,11 +391,11 @@ class MultilingualQuerySet(QuerySet):
|
|||
# Currently, we don't synchronize values of the original and default translation fields in that case.
|
||||
field_names_to_update = {field.name for field, *_ in values}
|
||||
|
||||
translation_values = []
|
||||
translation_values: list[tuple[Field, type[Model] | None, Any]] = []
|
||||
for field, model, value in values:
|
||||
translation_field_name = rewrite_lookup_key(self.model, field.name)
|
||||
if translation_field_name not in field_names_to_update:
|
||||
translatable_field = self.model._meta.get_field(translation_field_name)
|
||||
translatable_field = cast(Field, self.model._meta.get_field(translation_field_name))
|
||||
translation_values.append((translatable_field, model, value))
|
||||
|
||||
values += translation_values
|
||||
|
|
@ -396,14 +403,14 @@ class MultilingualQuerySet(QuerySet):
|
|||
|
||||
# This method was not present in django-linguo
|
||||
@property
|
||||
def _populate_mode(self):
|
||||
def _populate_mode(self) -> AutoPopulate:
|
||||
# Populate can be set using a global setting or a manager method.
|
||||
if self._populate is None:
|
||||
return auto_populate_mode()
|
||||
return self._populate
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def create(self, **kwargs):
|
||||
def create(self, **kwargs: Any) -> _T:
|
||||
"""
|
||||
Allows to override population mode with a ``populate`` method.
|
||||
"""
|
||||
|
|
@ -411,7 +418,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
return super().create(**kwargs)
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def get_or_create(self, *args, **kwargs):
|
||||
def get_or_create(self, *args: Any, **kwargs: Any) -> tuple[_T, bool]:
|
||||
"""
|
||||
Allows to override population mode with a ``populate`` method.
|
||||
"""
|
||||
|
|
@ -419,20 +426,20 @@ class MultilingualQuerySet(QuerySet):
|
|||
return super().get_or_create(*args, **kwargs)
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def defer(self, *fields):
|
||||
fields = append_lookup_keys(self.model, fields)
|
||||
def defer(self, *fields: Any) -> Self:
|
||||
fields = append_lookup_keys(self.model, fields) # type: ignore[assignment]
|
||||
return super().defer(*fields)
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def only(self, *fields):
|
||||
fields = append_lookup_keys(self.model, fields)
|
||||
def only(self, *fields: Any) -> Self:
|
||||
fields = append_lookup_keys(self.model, fields) # type: ignore[assignment]
|
||||
return super().only(*fields)
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def raw_values(self, *fields, **expressions):
|
||||
def raw_values(self, *fields: str, **expressions: Any) -> Self:
|
||||
return super().values(*fields, **expressions)
|
||||
|
||||
def _values(self, *original, **kwargs):
|
||||
def _values(self, *original: str, **kwargs: Any) -> Self:
|
||||
selects_all = kwargs.pop("selects_all", False)
|
||||
if not kwargs.pop("prepare", False):
|
||||
return super()._values(*original, **kwargs)
|
||||
|
|
@ -445,20 +452,20 @@ class MultilingualQuerySet(QuerySet):
|
|||
return clone
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def values(self, *fields, **expressions):
|
||||
def values(self, *fields: str, **expressions: Any) -> Self:
|
||||
if not self._rewrite:
|
||||
return super().values(*fields, **expressions)
|
||||
selects_all = not fields
|
||||
if not fields:
|
||||
# Emulate original queryset behaviour: get all fields that are not translation fields
|
||||
fields = self._get_original_fields()
|
||||
fields = self._get_original_fields() # type: ignore[assignment]
|
||||
fields += tuple(expressions)
|
||||
clone = self._values(*fields, prepare=True, selects_all=selects_all, **expressions)
|
||||
clone._iterable_class = FallbackValuesIterable
|
||||
return clone
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def values_list(self, *fields, flat=False, named=False):
|
||||
def values_list(self, *fields: str, flat: bool = False, named: bool = False) -> Self:
|
||||
if not self._rewrite:
|
||||
return super().values_list(*fields, flat=flat, named=named)
|
||||
if flat and named:
|
||||
|
|
@ -470,7 +477,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
selects_all = not fields
|
||||
if not fields:
|
||||
# Emulate original queryset behaviour: get all fields that are not translation fields
|
||||
fields = self._get_original_fields()
|
||||
fields = self._get_original_fields() # type: ignore[assignment]
|
||||
|
||||
field_names = {f for f in fields if not hasattr(f, "resolve_expression")}
|
||||
_fields = []
|
||||
|
|
@ -500,7 +507,7 @@ class MultilingualQuerySet(QuerySet):
|
|||
return clone
|
||||
|
||||
# This method was not present in django-linguo
|
||||
def dates(self, field_name, *args, **kwargs):
|
||||
def dates(self, field_name: str, *args: Any, **kwargs: Any) -> Self:
|
||||
if not self._rewrite:
|
||||
return super().dates(field_name, *args, **kwargs)
|
||||
new_key = rewrite_lookup_key(self.model, field_name)
|
||||
|
|
@ -508,11 +515,13 @@ class MultilingualQuerySet(QuerySet):
|
|||
|
||||
|
||||
class FallbackValuesIterable(ValuesIterable):
|
||||
queryset: MultilingualQuerySet[Model]
|
||||
|
||||
class X:
|
||||
# This stupid class is needed as object use __slots__ and has no __dict__.
|
||||
pass
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[dict[str, Any]]:
|
||||
instance = self.X()
|
||||
|
||||
fields = self.queryset.original_fields
|
||||
|
|
@ -527,13 +536,13 @@ class FallbackValuesIterable(ValuesIterable):
|
|||
|
||||
|
||||
class FallbackValuesListIterable(FallbackValuesIterable):
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[tuple[Any, ...]]:
|
||||
for row in super().__iter__():
|
||||
yield tuple(row.values())
|
||||
|
||||
|
||||
class FallbackNamedValuesListIterable(FallbackValuesIterable):
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[tuple[Any, ...]]:
|
||||
for row in super().__iter__():
|
||||
names, values = row.keys(), row.values()
|
||||
tuple_class = create_namedtuple_class(*names)
|
||||
|
|
@ -542,51 +551,68 @@ class FallbackNamedValuesListIterable(FallbackValuesIterable):
|
|||
|
||||
|
||||
class FallbackFlatValuesListIterable(FallbackValuesListIterable):
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Any]:
|
||||
for row in super().__iter__():
|
||||
yield row[0]
|
||||
|
||||
|
||||
def multilingual_queryset_factory(old_cls, instantiate=True):
|
||||
@overload
|
||||
def multilingual_queryset_factory(
|
||||
old_cls: type[Any], instantiate: Literal[False]
|
||||
) -> type[MultilingualQuerySet]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def multilingual_queryset_factory(
|
||||
old_cls: type[Any], instantiate: Literal[True] = ...
|
||||
) -> MultilingualQuerySet:
|
||||
...
|
||||
|
||||
|
||||
def multilingual_queryset_factory(
|
||||
old_cls: type[Any], instantiate: bool = True
|
||||
) -> type[MultilingualQuerySet] | MultilingualQuerySet:
|
||||
if old_cls == models.query.QuerySet:
|
||||
NewClass = MultilingualQuerySet
|
||||
else:
|
||||
|
||||
class NewClass(old_cls, MultilingualQuerySet):
|
||||
class NewClass(old_cls, MultilingualQuerySet): # type: ignore[no-redef]
|
||||
pass
|
||||
|
||||
NewClass.__name__ = "Multilingual%s" % old_cls.__name__
|
||||
return NewClass() if instantiate else NewClass
|
||||
|
||||
|
||||
class MultilingualQuerysetManager(models.Manager):
|
||||
class MultilingualQuerysetManager(models.Manager[_T]):
|
||||
"""
|
||||
This class gets hooked in MRO just before plain Manager, so that every call to
|
||||
get_queryset returns MultilingualQuerySet.
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
def get_queryset(self) -> MultilingualQuerySet[_T]:
|
||||
qs = super().get_queryset()
|
||||
return self._patch_queryset(qs)
|
||||
|
||||
def _patch_queryset(self, qs):
|
||||
def _patch_queryset(self, qs: QuerySet[_T]) -> MultilingualQuerySet[_T]:
|
||||
qs.__class__ = multilingual_queryset_factory(qs.__class__, instantiate=False)
|
||||
qs = cast(MultilingualQuerySet[_T], qs)
|
||||
qs._post_init()
|
||||
qs._rewrite_applied_operations()
|
||||
return qs
|
||||
|
||||
|
||||
class MultilingualManager(MultilingualQuerysetManager):
|
||||
def rewrite(self, *args, **kwargs):
|
||||
class MultilingualManager(MultilingualQuerysetManager[_T]):
|
||||
def rewrite(self, *args: Any, **kwargs: Any):
|
||||
return self.get_queryset().rewrite(*args, **kwargs)
|
||||
|
||||
def populate(self, *args, **kwargs):
|
||||
def populate(self, *args: Any, **kwargs: Any):
|
||||
return self.get_queryset().populate(*args, **kwargs)
|
||||
|
||||
def raw_values(self, *args, **kwargs):
|
||||
def raw_values(self, *args: Any, **kwargs: Any):
|
||||
return self.get_queryset().raw_values(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
def get_queryset(self) -> MultilingualQuerySet[_T]:
|
||||
"""
|
||||
This method is repeated because some managers that don't use super() or alter queryset class
|
||||
may return queryset that is not subclass of MultilingualQuerySet.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
def autodiscover():
|
||||
from typing import Any
|
||||
|
||||
|
||||
def autodiscover() -> None:
|
||||
"""
|
||||
Auto-discover INSTALLED_APPS translation.py modules and fail silently when
|
||||
not present. This forces an import on them to register.
|
||||
|
|
@ -57,7 +60,7 @@ def autodiscover():
|
|||
pass
|
||||
|
||||
|
||||
def handle_translation_registrations(*args, **kwargs):
|
||||
def handle_translation_registrations(*args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
Ensures that any configuration of the TranslationOption(s) are handled when
|
||||
importing modeltranslation.
|
||||
|
|
|
|||
|
|
@ -1,45 +1,57 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
TRANSLATION_FILES = tuple(getattr(settings, "MODELTRANSLATION_TRANSLATION_FILES", ()))
|
||||
from ._typing import AutoPopulate
|
||||
|
||||
AVAILABLE_LANGUAGES = list(
|
||||
TRANSLATION_FILES: tuple[str, ...] = tuple(
|
||||
getattr(settings, "MODELTRANSLATION_TRANSLATION_FILES", ())
|
||||
)
|
||||
|
||||
AVAILABLE_LANGUAGES: list[str] = list(
|
||||
getattr(
|
||||
settings,
|
||||
"MODELTRANSLATION_LANGUAGES",
|
||||
(val for val, label in settings.LANGUAGES),
|
||||
)
|
||||
)
|
||||
DEFAULT_LANGUAGE = getattr(settings, "MODELTRANSLATION_DEFAULT_LANGUAGE", None)
|
||||
if DEFAULT_LANGUAGE and DEFAULT_LANGUAGE not in AVAILABLE_LANGUAGES:
|
||||
_default_language: str | None = getattr(settings, "MODELTRANSLATION_DEFAULT_LANGUAGE", None)
|
||||
if _default_language and _default_language not in AVAILABLE_LANGUAGES:
|
||||
raise ImproperlyConfigured("MODELTRANSLATION_DEFAULT_LANGUAGE not in LANGUAGES setting.")
|
||||
elif not DEFAULT_LANGUAGE:
|
||||
DEFAULT_LANGUAGE = AVAILABLE_LANGUAGES[0]
|
||||
DEFAULT_LANGUAGE = _default_language or AVAILABLE_LANGUAGES[0]
|
||||
|
||||
# Fixed base language for prepopulated fields (slugs)
|
||||
# (If not set, the current request language will be used)
|
||||
PREPOPULATE_LANGUAGE = getattr(settings, "MODELTRANSLATION_PREPOPULATE_LANGUAGE", None)
|
||||
PREPOPULATE_LANGUAGE: str | None = getattr(settings, "MODELTRANSLATION_PREPOPULATE_LANGUAGE", None)
|
||||
if PREPOPULATE_LANGUAGE and PREPOPULATE_LANGUAGE not in AVAILABLE_LANGUAGES:
|
||||
raise ImproperlyConfigured("MODELTRANSLATION_PREPOPULATE_LANGUAGE not in LANGUAGES setting.")
|
||||
|
||||
# Load allowed CUSTOM_FIELDS from django settings
|
||||
CUSTOM_FIELDS = getattr(settings, "MODELTRANSLATION_CUSTOM_FIELDS", ())
|
||||
CUSTOM_FIELDS: tuple[str, ...] = getattr(settings, "MODELTRANSLATION_CUSTOM_FIELDS", ())
|
||||
|
||||
# Don't change this setting unless you really know what you are doing
|
||||
ENABLE_REGISTRATIONS = getattr(settings, "MODELTRANSLATION_ENABLE_REGISTRATIONS", settings.USE_I18N)
|
||||
ENABLE_REGISTRATIONS: bool = getattr(
|
||||
settings, "MODELTRANSLATION_ENABLE_REGISTRATIONS", settings.USE_I18N
|
||||
)
|
||||
|
||||
# Modeltranslation specific debug setting
|
||||
DEBUG = getattr(settings, "MODELTRANSLATION_DEBUG", False)
|
||||
DEBUG: bool = getattr(settings, "MODELTRANSLATION_DEBUG", False)
|
||||
|
||||
AUTO_POPULATE = getattr(settings, "MODELTRANSLATION_AUTO_POPULATE", False)
|
||||
AUTO_POPULATE: AutoPopulate = getattr(settings, "MODELTRANSLATION_AUTO_POPULATE", False)
|
||||
|
||||
# FALLBACK_LANGUAGES should be in either format:
|
||||
# MODELTRANSLATION_FALLBACK_LANGUAGES = ('en', 'de')
|
||||
# MODELTRANSLATION_FALLBACK_LANGUAGES = {'default': ('en', 'de'), 'fr': ('de',)}
|
||||
# By default we fallback to the default language
|
||||
FALLBACK_LANGUAGES = getattr(settings, "MODELTRANSLATION_FALLBACK_LANGUAGES", (DEFAULT_LANGUAGE,))
|
||||
if isinstance(FALLBACK_LANGUAGES, (tuple, list)):
|
||||
FALLBACK_LANGUAGES = {"default": tuple(FALLBACK_LANGUAGES)}
|
||||
_fallback_languages = getattr(settings, "MODELTRANSLATION_FALLBACK_LANGUAGES", (DEFAULT_LANGUAGE,))
|
||||
if isinstance(_fallback_languages, (tuple, list)):
|
||||
_fallback_languages = {"default": tuple(_fallback_languages)}
|
||||
|
||||
# To please mypy, explicitly annotate:
|
||||
FALLBACK_LANGUAGES: dict[str, tuple[str, ...]] = _fallback_languages
|
||||
|
||||
|
||||
if "default" not in FALLBACK_LANGUAGES:
|
||||
raise ImproperlyConfigured(
|
||||
'MODELTRANSLATION_FALLBACK_LANGUAGES does not contain "default" key.'
|
||||
|
|
@ -58,16 +70,16 @@ for key, value in FALLBACK_LANGUAGES.items():
|
|||
raise ImproperlyConfigured(
|
||||
'MODELTRANSLATION_FALLBACK_LANGUAGES: "%s" not in LANGUAGES setting.' % lang
|
||||
)
|
||||
ENABLE_FALLBACKS = getattr(settings, "MODELTRANSLATION_ENABLE_FALLBACKS", True)
|
||||
ENABLE_FALLBACKS: bool = getattr(settings, "MODELTRANSLATION_ENABLE_FALLBACKS", True)
|
||||
|
||||
LOADDATA_RETAIN_LOCALE = getattr(settings, "MODELTRANSLATION_LOADDATA_RETAIN_LOCALE", True)
|
||||
LOADDATA_RETAIN_LOCALE: bool = getattr(settings, "MODELTRANSLATION_LOADDATA_RETAIN_LOCALE", True)
|
||||
|
||||
JQUERY_URL = getattr(
|
||||
JQUERY_URL: str = getattr(
|
||||
settings,
|
||||
"JQUERY_URL",
|
||||
"//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js",
|
||||
)
|
||||
JQUERY_UI_URL = getattr(
|
||||
JQUERY_UI_URL: str = getattr(
|
||||
settings,
|
||||
"JQUERY_UI_URL",
|
||||
"//ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
import django.contrib.auth.models
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import modeltranslation.tests.models
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,8 @@ from django.test.utils import override_settings
|
|||
from django.utils.translation import get_language, override, trans_real
|
||||
from parameterized import parameterized
|
||||
|
||||
from modeltranslation import admin
|
||||
from modeltranslation import admin, translator
|
||||
from modeltranslation import settings as mt_settings
|
||||
from modeltranslation import translator
|
||||
from modeltranslation.forms import TranslationModelForm
|
||||
from modeltranslation.manager import MultilingualManager
|
||||
from modeltranslation.models import autodiscover
|
||||
|
|
|
|||
|
|
@ -1,27 +1,28 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Literal, Union
|
||||
|
||||
from modeltranslation import settings
|
||||
|
||||
AutoPopulate = Union[bool, Literal["all", "default", "required"]]
|
||||
from ._typing import AutoPopulate
|
||||
|
||||
|
||||
class ModelTranslationThreadLocal(threading.local):
|
||||
"""Holds thread-local data for modeltranslation."""
|
||||
|
||||
auto_populate: Union[AutoPopulate, None] = None
|
||||
enable_fallbacks: Union[bool, None] = None
|
||||
auto_populate: AutoPopulate | None = None
|
||||
enable_fallbacks: bool | None = None
|
||||
|
||||
|
||||
_mt_thread_context = ModelTranslationThreadLocal()
|
||||
|
||||
|
||||
def set_auto_populate(value: Union[AutoPopulate, None]) -> None:
|
||||
def set_auto_populate(value: AutoPopulate | None) -> None:
|
||||
"""Set the auto_populate for the current thread."""
|
||||
_mt_thread_context.auto_populate = value
|
||||
|
||||
|
||||
def set_enable_fallbacks(value: Union[bool, None]) -> None:
|
||||
def set_enable_fallbacks(value: bool | None) -> None:
|
||||
"""Set the enable_fallbacks for the current thread."""
|
||||
_mt_thread_context.enable_fallbacks = value
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from typing import Callable, Iterable
|
||||
from typing import Any, Callable, ClassVar, Collection, Iterable, Sequence, cast
|
||||
|
||||
import django
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import ForeignKey, Manager, ManyToManyField, OneToOneField, options
|
||||
from django.db.models import (
|
||||
Field,
|
||||
ForeignKey,
|
||||
Manager,
|
||||
ManyToManyField,
|
||||
Model,
|
||||
OneToOneField,
|
||||
options,
|
||||
)
|
||||
from django.db.models.base import ModelBase
|
||||
from django.db.models.signals import post_init
|
||||
from django.utils.functional import cached_property
|
||||
|
|
@ -15,6 +25,7 @@ from modeltranslation.fields import (
|
|||
TranslatedManyToManyDescriptor,
|
||||
TranslatedRelationIdDescriptor,
|
||||
TranslationFieldDescriptor,
|
||||
TranslationField,
|
||||
create_translation_field,
|
||||
)
|
||||
from modeltranslation.manager import (
|
||||
|
|
@ -26,6 +37,8 @@ from modeltranslation.manager import (
|
|||
from modeltranslation.thread_context import auto_populate_mode
|
||||
from modeltranslation.utils import build_localized_fieldname, parse_field
|
||||
|
||||
from ._typing import _ListOrTuple
|
||||
|
||||
|
||||
class AlreadyRegistered(Exception):
|
||||
pass
|
||||
|
|
@ -44,7 +57,7 @@ class FieldsAggregationMetaClass(type):
|
|||
Metaclass to handle custom inheritance of fields between classes.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type:
|
||||
attrs["fields"] = set(attrs.get("fields", ()))
|
||||
for base in bases:
|
||||
if isinstance(base, FieldsAggregationMetaClass):
|
||||
|
|
@ -74,20 +87,20 @@ class TranslationOptions(metaclass=FieldsAggregationMetaClass):
|
|||
``related_fields`` contains names of reverse lookup fields.
|
||||
"""
|
||||
|
||||
required_languages = ()
|
||||
required_languages: ClassVar[_ListOrTuple[str] | dict[str, _ListOrTuple[str]]] = ()
|
||||
|
||||
def __init__(self, model):
|
||||
def __init__(self, model: Model) -> None:
|
||||
"""
|
||||
Create fields dicts without any translation fields.
|
||||
"""
|
||||
self.model = model
|
||||
self.registered = False
|
||||
self.related = False
|
||||
self.local_fields = {f: set() for f in self.fields}
|
||||
self.fields = {f: set() for f in self.fields}
|
||||
self.related_fields = []
|
||||
self.local_fields: dict[str, set[TranslationField]] = {f: set() for f in self.fields}
|
||||
self.fields: dict[str, set[TranslationField]] = {f: set() for f in self.fields}
|
||||
self.related_fields: list[str] = []
|
||||
|
||||
def validate(self):
|
||||
def validate(self) -> None:
|
||||
"""
|
||||
Perform options validation.
|
||||
"""
|
||||
|
|
@ -104,14 +117,18 @@ class TranslationOptions(metaclass=FieldsAggregationMetaClass):
|
|||
"Fieldname in required_languages which is not in fields option."
|
||||
)
|
||||
|
||||
def _check_languages(self, languages, extra=()):
|
||||
def _check_languages(
|
||||
self,
|
||||
languages: Collection[str],
|
||||
extra: tuple[str, ...] = (),
|
||||
) -> None:
|
||||
correct = list(mt_settings.AVAILABLE_LANGUAGES) + list(extra)
|
||||
if any(lang not in correct for lang in languages):
|
||||
raise ImproperlyConfigured(
|
||||
"Language in required_languages which is not in AVAILABLE_LANGUAGES."
|
||||
)
|
||||
|
||||
def update(self, other):
|
||||
def update(self, other: TranslationOptions):
|
||||
"""
|
||||
Update with options from a superclass.
|
||||
"""
|
||||
|
|
@ -119,20 +136,20 @@ class TranslationOptions(metaclass=FieldsAggregationMetaClass):
|
|||
self.local_fields.update(other.local_fields)
|
||||
self.fields.update(other.fields)
|
||||
|
||||
def add_translation_field(self, field, translation_field):
|
||||
def add_translation_field(self, field: str, translation_field):
|
||||
"""
|
||||
Add a new translation field to both fields dicts.
|
||||
"""
|
||||
self.local_fields[field].add(translation_field)
|
||||
self.fields[field].add(translation_field)
|
||||
|
||||
def get_field_names(self):
|
||||
def get_field_names(self) -> list[str]:
|
||||
"""
|
||||
Return name of all fields that can be used in filtering.
|
||||
"""
|
||||
return list(self.fields.keys()) + self.related_fields
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
local = tuple(self.local_fields.keys())
|
||||
inherited = tuple(set(self.fields.keys()) - set(local))
|
||||
return "%s: %s + %s" % (self.__class__.__name__, local, inherited)
|
||||
|
|
@ -146,7 +163,7 @@ class MultilingualOptions(options.Options):
|
|||
return manager
|
||||
|
||||
|
||||
def add_translation_fields(model, opts):
|
||||
def add_translation_fields(model: type[Model], opts: TranslationOptions) -> None:
|
||||
"""
|
||||
Monkey patches the original model class to provide additional fields for
|
||||
every language.
|
||||
|
|
@ -229,7 +246,7 @@ def patch_manager_class(manager):
|
|||
manager.__class__ = NewMultilingualManager
|
||||
|
||||
|
||||
def add_manager(model):
|
||||
def add_manager(model: type[Model]) -> None:
|
||||
"""
|
||||
Monkey patches the original model to use MultilingualManager instead of
|
||||
default managers (not only ``objects``, but also every manager defined and inherited).
|
||||
|
|
@ -240,7 +257,7 @@ def add_manager(model):
|
|||
return
|
||||
# Make all managers local for this model to fix patching parent model managers
|
||||
added = set(model._meta.managers) - set(model._meta.local_managers)
|
||||
model._meta.local_managers = model._meta.managers
|
||||
model._meta.local_managers = model._meta.managers # type: ignore[assignment]
|
||||
|
||||
for current_manager in model._meta.local_managers:
|
||||
prev_class = current_manager.__class__
|
||||
|
|
@ -262,7 +279,7 @@ def add_manager(model):
|
|||
model._meta._expire_cache()
|
||||
|
||||
|
||||
def patch_constructor(model):
|
||||
def patch_constructor(model: type[Model]) -> None:
|
||||
"""
|
||||
Monkey patches the original model to rewrite fields names in __init__
|
||||
"""
|
||||
|
|
@ -280,19 +297,19 @@ def patch_constructor(model):
|
|||
model.__init__ = new_init
|
||||
|
||||
|
||||
def delete_mt_init(sender, instance, **kwargs):
|
||||
def delete_mt_init(sender: type[Model], instance: Model, **kwargs: Any) -> None:
|
||||
if hasattr(instance, "_mt_init"):
|
||||
del instance._mt_init
|
||||
|
||||
|
||||
def patch_clean_fields(model):
|
||||
def patch_clean_fields(model: type[Model]):
|
||||
"""
|
||||
Patch clean_fields method to handle different form types submission.
|
||||
"""
|
||||
old_clean_fields = model.clean_fields
|
||||
|
||||
def new_clean_fields(self, exclude=None):
|
||||
if hasattr(self, "_mt_form_pending_clear"):
|
||||
def new_clean_fields(self, exclude: Collection[str] | None = None) -> None:
|
||||
if hasattr(self, "_mt_form_pending_clear") and exclude is not None:
|
||||
# Some form translation fields has been marked as clearing value.
|
||||
# Check if corresponding translated field was also saved (not excluded):
|
||||
# - if yes, it seems like form for MT-unaware app. Ignore clearing (left value from
|
||||
|
|
@ -313,7 +330,7 @@ def patch_clean_fields(model):
|
|||
model.clean_fields = new_clean_fields
|
||||
|
||||
|
||||
def patch_get_deferred_fields(model):
|
||||
def patch_get_deferred_fields(model: type[Model]) -> None:
|
||||
"""
|
||||
Django >= 1.8: patch detecting deferred fields. Crucial for only/defer to work.
|
||||
"""
|
||||
|
|
@ -321,7 +338,7 @@ def patch_get_deferred_fields(model):
|
|||
return
|
||||
old_get_deferred_fields = model.get_deferred_fields
|
||||
|
||||
def new_get_deferred_fields(self):
|
||||
def new_get_deferred_fields(self) -> set[str]:
|
||||
sup = old_get_deferred_fields(self)
|
||||
if hasattr(self, "_fields_were_deferred"):
|
||||
sup.update(self._fields_were_deferred)
|
||||
|
|
@ -330,7 +347,7 @@ def patch_get_deferred_fields(model):
|
|||
model.get_deferred_fields = new_get_deferred_fields
|
||||
|
||||
|
||||
def patch_refresh_from_db(model):
|
||||
def patch_refresh_from_db(model: type[Model]) -> None:
|
||||
"""
|
||||
Django >= 1.10: patch refreshing deferred fields. Crucial for only/defer to work.
|
||||
"""
|
||||
|
|
@ -338,15 +355,17 @@ def patch_refresh_from_db(model):
|
|||
return
|
||||
old_refresh_from_db = model.refresh_from_db
|
||||
|
||||
def new_refresh_from_db(self, using=None, fields=None):
|
||||
def new_refresh_from_db(
|
||||
self, using: str | None = None, fields: Sequence[str] | None = None
|
||||
) -> None:
|
||||
if fields is not None:
|
||||
fields = append_translated(self.__class__, fields)
|
||||
fields = append_translated(self.__class__, fields) # type: ignore[assignment]
|
||||
return old_refresh_from_db(self, using, fields)
|
||||
|
||||
model.refresh_from_db = new_refresh_from_db
|
||||
|
||||
|
||||
def delete_cache_fields(model):
|
||||
def delete_cache_fields(model: type[Model]) -> None:
|
||||
opts = model._meta
|
||||
cached_attrs = (
|
||||
"_field_cache",
|
||||
|
|
@ -366,7 +385,7 @@ def delete_cache_fields(model):
|
|||
model._meta._expire_cache()
|
||||
|
||||
|
||||
def populate_translation_fields(sender, kwargs):
|
||||
def populate_translation_fields(sender: type[Model], kwargs: Any):
|
||||
"""
|
||||
When models are created or loaded from fixtures, replicates values
|
||||
provided for translatable fields to some / all empty translation fields,
|
||||
|
|
@ -410,7 +429,7 @@ def populate_translation_fields(sender, kwargs):
|
|||
kwargs.setdefault(default, val)
|
||||
elif populate == "required":
|
||||
default = build_localized_fieldname(key, mt_settings.DEFAULT_LANGUAGE)
|
||||
if not sender._meta.get_field(key).null:
|
||||
if not sender._meta.get_field(key).null: # type: ignore[union-attr]
|
||||
kwargs.setdefault(default, val)
|
||||
else:
|
||||
raise AttributeError("Unknown population mode '%s'." % populate)
|
||||
|
|
@ -441,13 +460,18 @@ class Translator:
|
|||
registered with the Translator using the register() method.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
# All seen models (model class -> ``TranslationOptions`` instance).
|
||||
self._registry = {}
|
||||
self._registry: dict[type[Model], TranslationOptions] = {}
|
||||
# List of funcs to execute after all imports are done.
|
||||
self._lazy_operations: Iterable[Callable] = []
|
||||
self._lazy_operations: list[Callable[..., Any]] = []
|
||||
|
||||
def register(self, model_or_iterable, opts_class=None, **options):
|
||||
def register(
|
||||
self,
|
||||
model_or_iterable: type[Model] | Iterable[type[Model]],
|
||||
opts_class: type[TranslationOptions] | None = None,
|
||||
**options: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Registers the given model(s) with the given translation options.
|
||||
|
||||
|
|
@ -491,7 +515,7 @@ class Translator:
|
|||
self._registry[model].registered = False
|
||||
raise
|
||||
|
||||
def _register_single_model(self, model, opts):
|
||||
def _register_single_model(self, model: type[Model], opts: TranslationOptions) -> None:
|
||||
# Now, when all fields are initialized and inherited, validate configuration.
|
||||
opts.validate()
|
||||
|
||||
|
|
@ -536,7 +560,7 @@ class Translator:
|
|||
model_fallback_values = getattr(opts, "fallback_values", NONE)
|
||||
model_fallback_undefined = getattr(opts, "fallback_undefined", NONE)
|
||||
for field_name in opts.local_fields.keys():
|
||||
field = model._meta.get_field(field_name)
|
||||
field = cast(Field, model._meta.get_field(field_name))
|
||||
field_fallback_value = parse_field(model_fallback_values, field_name, NONE)
|
||||
field_fallback_undefined = parse_field(model_fallback_undefined, field_name, NONE)
|
||||
descriptor = TranslationFieldDescriptor(
|
||||
|
|
@ -569,11 +593,12 @@ class Translator:
|
|||
if isinstance(field, OneToOneField):
|
||||
# Fix translated_field caching for SingleRelatedObjectDescriptor
|
||||
sro_descriptor = getattr(
|
||||
field.remote_field.model, field.remote_field.get_accessor_name()
|
||||
field.remote_field.model,
|
||||
field.remote_field.get_accessor_name(), # type: ignore[arg-type]
|
||||
)
|
||||
patch_related_object_descriptor_caching(sro_descriptor)
|
||||
|
||||
def unregister(self, model_or_iterable):
|
||||
def unregister(self, model_or_iterable: type[Model] | Iterable[type[Model]]) -> None:
|
||||
"""
|
||||
Unregisters the given model(s).
|
||||
|
||||
|
|
@ -600,7 +625,7 @@ class Translator:
|
|||
)
|
||||
del self._registry[desc]
|
||||
|
||||
def get_registered_models(self, abstract=True):
|
||||
def get_registered_models(self, abstract: bool = True) -> list[type[Model]]:
|
||||
"""
|
||||
Returns a list of all registered models, or just concrete
|
||||
registered models.
|
||||
|
|
@ -611,7 +636,9 @@ class Translator:
|
|||
if opts.registered and (not model._meta.abstract or abstract)
|
||||
]
|
||||
|
||||
def _get_options_for_model(self, model, opts_class=None, **options):
|
||||
def _get_options_for_model(
|
||||
self, model: type[Model], opts_class: type[TranslationOptions] | None = None, **options: Any
|
||||
) -> TranslationOptions:
|
||||
"""
|
||||
Returns an instance of translation options with translated fields
|
||||
defined for the ``model`` and inherited from superclasses.
|
||||
|
|
@ -640,7 +667,7 @@ class Translator:
|
|||
|
||||
return self._registry[model]
|
||||
|
||||
def get_options_for_model(self, model) -> TranslationOptions:
|
||||
def get_options_for_model(self, model: type[Model]) -> TranslationOptions:
|
||||
"""
|
||||
Thin wrapper around ``_get_options_for_model`` to preserve the
|
||||
semantic of throwing exception for models not directly registered.
|
||||
|
|
@ -656,7 +683,7 @@ class Translator:
|
|||
while self._lazy_operations:
|
||||
self._lazy_operations.pop(0)(translator=self)
|
||||
|
||||
def lazy_operation(self, func: Callable, *args, **kwargs) -> None:
|
||||
def lazy_operation(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
|
||||
self._lazy_operations.append(partial(func, *args, **kwargs))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Generator, Iterable, Iterator, TypeVar
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_str
|
||||
|
|
@ -13,8 +16,12 @@ from modeltranslation.thread_context import (
|
|||
set_enable_fallbacks,
|
||||
)
|
||||
|
||||
from ._typing import AutoPopulate
|
||||
|
||||
def get_language():
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def get_language() -> str:
|
||||
"""
|
||||
Return an active language code that is guaranteed to be in
|
||||
settings.LANGUAGES (Django does not seem to guarantee this for us).
|
||||
|
|
@ -29,7 +36,7 @@ def get_language():
|
|||
return settings.DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def get_language_bidi(lang):
|
||||
def get_language_bidi(lang: str) -> bool:
|
||||
"""
|
||||
Check if a language is bi-directional.
|
||||
"""
|
||||
|
|
@ -37,14 +44,14 @@ def get_language_bidi(lang):
|
|||
return lang_info["bidi"]
|
||||
|
||||
|
||||
def get_translation_fields(field):
|
||||
def get_translation_fields(field: str) -> list[str]:
|
||||
"""
|
||||
Returns a list of localized fieldnames for a given field.
|
||||
"""
|
||||
return [build_localized_fieldname(field, lang) for lang in settings.AVAILABLE_LANGUAGES]
|
||||
|
||||
|
||||
def build_localized_fieldname(field_name, lang):
|
||||
def build_localized_fieldname(field_name: str, lang: str) -> str:
|
||||
if lang == "id":
|
||||
# The 2-letter Indonesian language code is problematic with the
|
||||
# current naming scheme as Django foreign keys also add "id" suffix.
|
||||
|
|
@ -52,7 +59,7 @@ def build_localized_fieldname(field_name, lang):
|
|||
return str("%s_%s" % (field_name, lang.replace("-", "_")))
|
||||
|
||||
|
||||
def _build_localized_verbose_name(verbose_name, lang):
|
||||
def _build_localized_verbose_name(verbose_name: Any, lang: str) -> str:
|
||||
if lang == "id":
|
||||
lang = "ind"
|
||||
return force_str("%s [%s]") % (force_str(verbose_name), lang)
|
||||
|
|
@ -61,13 +68,13 @@ def _build_localized_verbose_name(verbose_name, lang):
|
|||
build_localized_verbose_name = lazy(_build_localized_verbose_name, str)
|
||||
|
||||
|
||||
def _join_css_class(bits, offset):
|
||||
def _join_css_class(bits: list[str], offset: int) -> str:
|
||||
if "-".join(bits[-offset:]) in settings.AVAILABLE_LANGUAGES + ["en-us"]:
|
||||
return "%s-%s" % ("_".join(bits[: len(bits) - offset]), "_".join(bits[-offset:]))
|
||||
return ""
|
||||
|
||||
|
||||
def build_css_class(localized_fieldname, prefix=""):
|
||||
def build_css_class(localized_fieldname: str, prefix: str = ""):
|
||||
"""
|
||||
Returns a css class based on ``localized_fieldname`` which is easily
|
||||
splittable and capable of regionalized language codes.
|
||||
|
|
@ -99,7 +106,7 @@ def build_css_class(localized_fieldname, prefix=""):
|
|||
return "%s-%s" % (prefix, css_class) if prefix else css_class
|
||||
|
||||
|
||||
def unique(seq):
|
||||
def unique(seq: Iterable[_T]) -> Generator[_T, None, None]:
|
||||
"""
|
||||
Returns a generator yielding unique sequence members in order
|
||||
|
||||
|
|
@ -109,10 +116,12 @@ def unique(seq):
|
|||
[1, 2, 3, 4]
|
||||
"""
|
||||
seen = set()
|
||||
return (x for x in seq if x not in seen and not seen.add(x))
|
||||
return (x for x in seq if x not in seen and not seen.add(x)) # type: ignore[func-returns-value]
|
||||
|
||||
|
||||
def resolution_order(lang, override=None):
|
||||
def resolution_order(
|
||||
lang: str, override: dict[str, tuple[str, ...]] | None = None
|
||||
) -> tuple[str, ...]:
|
||||
"""
|
||||
Return order of languages which should be checked for parameter language.
|
||||
First is always the parameter language, later are fallback languages.
|
||||
|
|
@ -130,7 +139,7 @@ def resolution_order(lang, override=None):
|
|||
|
||||
|
||||
@contextmanager
|
||||
def auto_populate(mode="all"):
|
||||
def auto_populate(mode: AutoPopulate = "all") -> Iterator[None]:
|
||||
"""
|
||||
Overrides translation fields population mode (population mode decides which
|
||||
unprovided translations will be filled during model construction / loading).
|
||||
|
|
@ -156,7 +165,7 @@ def auto_populate(mode="all"):
|
|||
|
||||
|
||||
@contextmanager
|
||||
def fallbacks(enable=True):
|
||||
def fallbacks(enable: bool | None = True) -> Iterator[None]:
|
||||
"""
|
||||
Temporarily switch all language fallbacks on or off.
|
||||
|
||||
|
|
@ -176,7 +185,7 @@ def fallbacks(enable=True):
|
|||
set_enable_fallbacks(None)
|
||||
|
||||
|
||||
def parse_field(setting, field_name, default):
|
||||
def parse_field(setting: Any | dict[str, Any], field_name: str, default: Any) -> Any:
|
||||
"""
|
||||
Extract result from single-value or dict-type setting like fallback_values.
|
||||
"""
|
||||
|
|
@ -186,7 +195,9 @@ def parse_field(setting, field_name, default):
|
|||
return setting
|
||||
|
||||
|
||||
def build_localized_intermediary_model(intermediary_model: models.Model, lang: str) -> models.Model:
|
||||
def build_localized_intermediary_model(
|
||||
intermediary_model: type[models.Model], lang: str
|
||||
) -> type[models.Model]:
|
||||
from modeltranslation.translator import translator
|
||||
|
||||
meta = type(
|
||||
|
|
@ -212,7 +223,7 @@ def build_localized_intermediary_model(intermediary_model: models.Model, lang: s
|
|||
(models.Model,),
|
||||
{
|
||||
**{k: v for k, v in dict(intermediary_model.__dict__).items() if k != "_meta"},
|
||||
**{f.name: f.clone() for f in intermediary_model._meta.fields},
|
||||
**{f.name: f.clone() for f in intermediary_model._meta.fields}, # type: ignore[attr-defined]
|
||||
"Meta": meta,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.forms.renderers import BaseRenderer
|
||||
from django.forms.widgets import CheckboxInput, Media, Widget
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -30,7 +37,7 @@ class ClearableWidgetWrapper(Widget):
|
|||
class Media:
|
||||
js = ("modeltranslation/js/clearable_inputs.js",)
|
||||
|
||||
def __init__(self, widget, empty_value=None):
|
||||
def __init__(self, widget: Widget, empty_value: Any | None = None) -> None:
|
||||
"""
|
||||
Remebers the widget we are wrapping and precreates a checkbox input.
|
||||
Allows overriding the empty value.
|
||||
|
|
@ -39,7 +46,7 @@ class ClearableWidgetWrapper(Widget):
|
|||
self.checkbox = CheckboxInput(attrs={"tabindex": "-1"})
|
||||
self.empty_value = empty_value
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""
|
||||
If we don't have a property or a method, chances are the wrapped
|
||||
widget does.
|
||||
|
|
@ -56,7 +63,13 @@ class ClearableWidgetWrapper(Widget):
|
|||
"""
|
||||
return self.widget.media + self.checkbox.media + Media(self.Media)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
def render(
|
||||
self,
|
||||
name: str,
|
||||
value: Any,
|
||||
attrs: dict[str, Any] | None = None,
|
||||
renderer: BaseRenderer | None = None,
|
||||
) -> SafeString:
|
||||
"""
|
||||
Appends a checkbox for clearing the value (that is, setting the field
|
||||
with the ``empty_value``).
|
||||
|
|
@ -77,7 +90,9 @@ class ClearableWidgetWrapper(Widget):
|
|||
)
|
||||
)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
def value_from_datadict(
|
||||
self, data: Mapping[str, Any], files: MultiValueDict[str, UploadedFile], name: str
|
||||
) -> Any:
|
||||
"""
|
||||
If the clear checkbox is checked returns the configured empty value,
|
||||
completely ignoring the original input.
|
||||
|
|
@ -87,13 +102,13 @@ class ClearableWidgetWrapper(Widget):
|
|||
return self.empty_value
|
||||
return self.widget.value_from_datadict(data, files, name)
|
||||
|
||||
def clear_checkbox_name(self, name):
|
||||
def clear_checkbox_name(self, name: str) -> str:
|
||||
"""
|
||||
Given the name of the input, returns the name of the clear checkbox.
|
||||
"""
|
||||
return name + "-clear"
|
||||
|
||||
def clear_checkbox_id(self, name):
|
||||
def clear_checkbox_id(self, name: str) -> str:
|
||||
"""
|
||||
Given the name of the clear checkbox input, returns the HTML id for it.
|
||||
"""
|
||||
|
|
|
|||
124
poetry.lock
generated
124
poetry.lock
generated
|
|
@ -163,18 +163,41 @@ argon2 = ["argon2-cffi (>=19.1.0)"]
|
|||
bcrypt = ["bcrypt"]
|
||||
|
||||
[[package]]
|
||||
name = "django-types"
|
||||
version = "0.19.1"
|
||||
description = "Type stubs for Django"
|
||||
name = "django-stubs"
|
||||
version = "4.2.7"
|
||||
description = "Mypy stubs for Django"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "django_types-0.19.1-py3-none-any.whl", hash = "sha256:b3f529de17f6374d41ca67232aa01330c531bbbaa3ac4097896f31ac33c96c30"},
|
||||
{file = "django_types-0.19.1.tar.gz", hash = "sha256:5ae7988612cf6fbc357b018bbc3b3a878b65e04275cc46e0d35d66a708daff12"},
|
||||
{file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"},
|
||||
{file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
types-psycopg2 = ">=2.9.21.13"
|
||||
django = "*"
|
||||
django-stubs-ext = ">=4.2.7"
|
||||
tomli = {version = "*", markers = "python_version < \"3.11\""}
|
||||
types-pytz = "*"
|
||||
types-PyYAML = "*"
|
||||
typing-extensions = "*"
|
||||
|
||||
[package.extras]
|
||||
compatible-mypy = ["mypy (>=1.7.0,<1.8.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "django-stubs-ext"
|
||||
version = "4.2.7"
|
||||
description = "Monkey-patching and extensions for django-stubs"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "django-stubs-ext-4.2.7.tar.gz", hash = "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3"},
|
||||
{file = "django_stubs_ext-4.2.7-py3-none-any.whl", hash = "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
django = "*"
|
||||
typing-extensions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
|
|
@ -216,6 +239,64 @@ files = [
|
|||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.8.0"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"},
|
||||
{file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"},
|
||||
{file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"},
|
||||
{file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"},
|
||||
{file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"},
|
||||
{file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"},
|
||||
{file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"},
|
||||
{file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"},
|
||||
{file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"},
|
||||
{file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"},
|
||||
{file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mypy-extensions = ">=1.0.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = ">=4.1.0"
|
||||
|
||||
[package.extras]
|
||||
dmypy = ["psutil (>=4.0)"]
|
||||
install-types = ["pip"]
|
||||
mypyc = ["setuptools (>=50)"]
|
||||
reports = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
|
|
@ -455,25 +536,36 @@ files = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-psycopg2"
|
||||
version = "2.9.21.20240218"
|
||||
description = "Typing stubs for psycopg2"
|
||||
name = "types-pytz"
|
||||
version = "2024.1.0.20240203"
|
||||
description = "Typing stubs for pytz"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "types-psycopg2-2.9.21.20240218.tar.gz", hash = "sha256:3084cd807038a62c80fb5be78b41d855b48a060316101ea59fd85c302efb57d4"},
|
||||
{file = "types_psycopg2-2.9.21.20240218-py3-none-any.whl", hash = "sha256:cac96264e063cbce28dee337a973d39e6df4ca671252343cb4f8e5ef6db5e67d"},
|
||||
{file = "types-pytz-2024.1.0.20240203.tar.gz", hash = "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e"},
|
||||
{file = "types_pytz-2024.1.0.20240203-py3-none-any.whl", hash = "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-pyyaml"
|
||||
version = "6.0.12.12"
|
||||
description = "Typing stubs for PyYAML"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"},
|
||||
{file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.8.0"
|
||||
version = "4.9.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
|
||||
{file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
|
||||
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
|
||||
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -507,4 +599,4 @@ test = ["pytest"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.8,<4"
|
||||
content-hash = "1e5d8f3a7c1a4e2bbff30e4030c0bbb35ea30fce8f47e3977d6a41757b3dba2f"
|
||||
content-hash = "e0e62ada8d6bf5e773ac7baee0f04747335ae082f9c3b23db06cb59f9f647e9b"
|
||||
|
|
|
|||
|
|
@ -12,15 +12,17 @@ packages = [
|
|||
python = ">=3.8,<4"
|
||||
django = ">=4.2"
|
||||
ruff = "^0.0.292"
|
||||
typing-extensions = {version = ">=4.0.1", python = "<3.11"}
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pdbpp = "*"
|
||||
parameterized = "*"
|
||||
pytest-cov = "*"
|
||||
pytest = "*"
|
||||
pytest-sugar = "*"
|
||||
pytest-django = "*"
|
||||
django-types = "*"
|
||||
django-stubs = "^4.2.7"
|
||||
mypy = "^1.8.0"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
|
@ -28,3 +30,32 @@ target-version = "py38"
|
|||
ignore = [
|
||||
"E501", # line length is handled by formatter
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
incremental = true
|
||||
warn_unused_ignores = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_configs = true
|
||||
pretty = true
|
||||
show_error_context = true
|
||||
exclude = [
|
||||
'tests/'
|
||||
]
|
||||
disable_error_code = ["method-assign"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["modeltranslation.fields"]
|
||||
disable_error_code = ["attr-defined", "has-type", "misc"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["modeltranslation.admin"]
|
||||
disable_error_code = ["override", "attr-defined"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["modeltranslation.translator"]
|
||||
disable_error_code = ["override", "attr-defined"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["modeltranslation.manager"]
|
||||
disable_error_code = ["override", "attr-defined", "return-value", "misc"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"include": ["."],
|
||||
"pythonVersion": "3.7",
|
||||
"pythonVersion": "3.8",
|
||||
"pythonPlatform": "Linux",
|
||||
"useLibraryCodeForTypes": true,
|
||||
"strictListInference": true,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ license = New BSD
|
|||
[options]
|
||||
install_requires =
|
||||
Django>=4.2
|
||||
typing-extensions>=4.0.1; python_version<"3.11"
|
||||
packages =
|
||||
modeltranslation
|
||||
modeltranslation.management
|
||||
|
|
|
|||
Loading…
Reference in a new issue