Initial commit
This commit is contained in:
487
env/lib/python3.10/site-packages/wagtail/models/i18n.py
vendored
Normal file
487
env/lib/python3.10/site-packages/wagtail/models/i18n.py
vendored
Normal file
@@ -0,0 +1,487 @@
|
||||
import uuid
|
||||
from typing import Dict
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core import checks
|
||||
from django.db import migrations, models, transaction
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import translation
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from modelcluster.fields import ParentalKey
|
||||
|
||||
from wagtail.actions.copy_for_translation import CopyForTranslationAction
|
||||
from wagtail.coreutils import (
|
||||
get_content_languages,
|
||||
get_supported_content_language_variant,
|
||||
)
|
||||
from wagtail.signals import pre_validate_delete
|
||||
|
||||
|
||||
def pk(obj):
|
||||
if isinstance(obj, models.Model):
|
||||
return obj.pk
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
class LocaleManager(models.Manager):
|
||||
def get_for_language(self, language_code):
|
||||
"""
|
||||
Gets a Locale from a language code.
|
||||
"""
|
||||
return self.get(
|
||||
language_code=get_supported_content_language_variant(language_code)
|
||||
)
|
||||
|
||||
|
||||
class Locale(models.Model):
|
||||
#: The language code that represents this locale
|
||||
#:
|
||||
#: The language code can either be a language code on its own (such as ``en``, ``fr``),
|
||||
#: or it can include a region code (such as ``en-gb``, ``fr-fr``).
|
||||
language_code = models.CharField(max_length=100, unique=True)
|
||||
|
||||
# Objects excludes any Locales that have been removed from LANGUAGES, This effectively disables them
|
||||
# The Locale management UI needs to be able to see these so we provide a separate manager `all_objects`
|
||||
objects = LocaleManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
ordering = [
|
||||
"language_code",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_default(cls):
|
||||
"""
|
||||
Returns the default Locale based on the site's LANGUAGE_CODE setting
|
||||
"""
|
||||
return cls.objects.get_for_language(settings.LANGUAGE_CODE)
|
||||
|
||||
@classmethod
|
||||
def get_active(cls):
|
||||
"""
|
||||
Returns the Locale that corresponds to the currently activated language in Django.
|
||||
"""
|
||||
try:
|
||||
return cls.objects.get_for_language(translation.get_language())
|
||||
except (cls.DoesNotExist, LookupError):
|
||||
return cls.get_default()
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, *args, **kwargs):
|
||||
# Provide a signal like pre_delete, but sent before on_delete validation.
|
||||
# This allows us to use the signal to fix up references to the locale to be deleted
|
||||
# that would otherwise fail validation.
|
||||
# Workaround for https://code.djangoproject.com/ticket/6870
|
||||
pre_validate_delete.send(sender=Locale, instance=self)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def language_code_is_valid(self):
|
||||
return self.language_code in get_content_languages()
|
||||
|
||||
def get_display_name(self) -> str:
|
||||
try:
|
||||
return get_content_languages()[self.language_code]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
return self.language_name
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return self.language_code
|
||||
|
||||
def __str__(self):
|
||||
return force_str(self.get_display_name())
|
||||
|
||||
def _get_language_info(self) -> Dict[str, str]:
|
||||
return translation.get_language_info(self.language_code)
|
||||
|
||||
@property
|
||||
def language_info(self):
|
||||
return translation.get_language_info(self.language_code)
|
||||
|
||||
@property
|
||||
def language_name(self):
|
||||
"""
|
||||
Uses data from ``django.conf.locale`` to return the language name in
|
||||
English. For example, if the object's ``language_code`` were ``"fr"``,
|
||||
the return value would be ``"French"``.
|
||||
|
||||
Raises ``KeyError`` if ``django.conf.locale`` has no information
|
||||
for the object's ``language_code`` value.
|
||||
"""
|
||||
return self.language_info["name"]
|
||||
|
||||
@property
|
||||
def language_name_local(self):
|
||||
"""
|
||||
Uses data from ``django.conf.locale`` to return the language name in
|
||||
the language itself. For example, if the ``language_code`` were
|
||||
``"fr"`` (French), the return value would be ``"français"``.
|
||||
|
||||
Raises ``KeyError`` if ``django.conf.locale`` has no information
|
||||
for the object's ``language_code`` value.
|
||||
"""
|
||||
return self.language_info["name_local"]
|
||||
|
||||
@property
|
||||
def language_name_localized(self):
|
||||
"""
|
||||
Uses data from ``django.conf.locale`` to return the language name in
|
||||
the currently active language. For example, if ``language_code`` were
|
||||
``"fr"`` (French), and the active language were ``"da"`` (Danish), the
|
||||
return value would be ``"Fransk"``.
|
||||
|
||||
Raises ``KeyError`` if ``django.conf.locale`` has no information
|
||||
for the object's ``language_code`` value.
|
||||
|
||||
"""
|
||||
return translation.gettext(self.language_name)
|
||||
|
||||
@property
|
||||
def is_bidi(self) -> bool:
|
||||
"""
|
||||
Returns a boolean indicating whether the language is bi-directional.
|
||||
"""
|
||||
return self.language_code in settings.LANGUAGES_BIDI
|
||||
|
||||
@property
|
||||
def is_default(self) -> bool:
|
||||
"""
|
||||
Returns a boolean indicating whether this object is the default locale.
|
||||
"""
|
||||
try:
|
||||
return self.language_code == get_supported_content_language_variant(
|
||||
settings.LANGUAGE_CODE
|
||||
)
|
||||
except LookupError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
"""
|
||||
Returns a boolean indicating whether this object is the currently active locale.
|
||||
"""
|
||||
try:
|
||||
return self.language_code == get_supported_content_language_variant(
|
||||
translation.get_language()
|
||||
)
|
||||
except LookupError:
|
||||
return self.is_default
|
||||
|
||||
|
||||
class TranslatableMixin(models.Model):
|
||||
translation_key = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
locale = models.ForeignKey(
|
||||
Locale,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="+",
|
||||
editable=False,
|
||||
verbose_name=_("locale"),
|
||||
)
|
||||
locale.wagtail_reference_index_ignore = True
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
unique_together = [("translation_key", "locale")]
|
||||
|
||||
@classmethod
|
||||
def check(cls, **kwargs):
|
||||
errors = super().check(**kwargs)
|
||||
# No need to check on multi-table-inheritance children as it only needs to be applied to
|
||||
# the table that has the translation_key/locale fields
|
||||
is_translation_model = cls.get_translation_model() is cls
|
||||
if not is_translation_model:
|
||||
return errors
|
||||
|
||||
unique_constraint_fields = ("translation_key", "locale")
|
||||
|
||||
has_unique_constraint = any(
|
||||
isinstance(constraint, models.UniqueConstraint)
|
||||
and set(constraint.fields) == set(unique_constraint_fields)
|
||||
for constraint in cls._meta.constraints
|
||||
)
|
||||
|
||||
has_unique_together = unique_constraint_fields in cls._meta.unique_together
|
||||
|
||||
# Raise error if subclass has removed constraints
|
||||
if not (has_unique_constraint or has_unique_together):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
"%s is missing a UniqueConstraint for the fields: %s."
|
||||
% (cls._meta.label, unique_constraint_fields),
|
||||
hint=(
|
||||
"Add models.UniqueConstraint(fields=%s, "
|
||||
"name='unique_translation_key_locale_%s_%s') to %s.Meta.constraints."
|
||||
% (
|
||||
unique_constraint_fields,
|
||||
cls._meta.app_label,
|
||||
cls._meta.model_name,
|
||||
cls.__name__,
|
||||
)
|
||||
),
|
||||
obj=cls,
|
||||
id="wagtailcore.E003",
|
||||
)
|
||||
)
|
||||
|
||||
# Raise error if subclass has both UniqueConstraint and unique_together
|
||||
if has_unique_constraint and has_unique_together:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
"%s should not have both UniqueConstraint and unique_together for: %s."
|
||||
% (cls._meta.label, unique_constraint_fields),
|
||||
hint="Remove unique_together in favor of UniqueConstraint.",
|
||||
obj=cls,
|
||||
id="wagtailcore.E003",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
@property
|
||||
def localized(self):
|
||||
"""
|
||||
Finds the translation in the current active language.
|
||||
|
||||
If there is no translation in the active language, self is returned.
|
||||
|
||||
Note: This will not return the translation if it is in draft.
|
||||
If you want to include drafts, use the ``.localized_draft`` attribute instead.
|
||||
"""
|
||||
from wagtail.models import DraftStateMixin
|
||||
|
||||
localized = self.localized_draft
|
||||
if isinstance(self, DraftStateMixin) and not localized.live:
|
||||
return self
|
||||
|
||||
return localized
|
||||
|
||||
@property
|
||||
def localized_draft(self):
|
||||
"""
|
||||
Finds the translation in the current active language.
|
||||
|
||||
If there is no translation in the active language, self is returned.
|
||||
|
||||
Note: This will return translations that are in draft. If you want to exclude
|
||||
these, use the ``.localized`` attribute.
|
||||
"""
|
||||
if not getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
return self
|
||||
|
||||
try:
|
||||
locale = Locale.get_active()
|
||||
except (LookupError, Locale.DoesNotExist):
|
||||
return self
|
||||
|
||||
if locale.id == self.locale_id:
|
||||
return self
|
||||
|
||||
return self.get_translation_or_none(locale) or self
|
||||
|
||||
def get_translations(self, inclusive=False):
|
||||
"""
|
||||
Returns a queryset containing the translations of this instance.
|
||||
"""
|
||||
translations = self.__class__.objects.filter(
|
||||
translation_key=self.translation_key
|
||||
)
|
||||
|
||||
if inclusive is False:
|
||||
translations = translations.exclude(id=self.id)
|
||||
|
||||
return translations
|
||||
|
||||
def get_translation(self, locale):
|
||||
"""
|
||||
Finds the translation in the specified locale.
|
||||
|
||||
If there is no translation in that locale, this raises a ``model.DoesNotExist`` exception.
|
||||
"""
|
||||
return self.get_translations(inclusive=True).get(locale_id=pk(locale))
|
||||
|
||||
def get_translation_or_none(self, locale):
|
||||
"""
|
||||
Finds the translation in the specified locale.
|
||||
|
||||
If there is no translation in that locale, this returns None.
|
||||
"""
|
||||
try:
|
||||
return self.get_translation(locale)
|
||||
except self.__class__.DoesNotExist:
|
||||
return None
|
||||
|
||||
def has_translation(self, locale):
|
||||
"""
|
||||
Returns True if a translation exists in the specified locale.
|
||||
"""
|
||||
return (
|
||||
self.get_translations(inclusive=True).filter(locale_id=pk(locale)).exists()
|
||||
)
|
||||
|
||||
def copy_for_translation(self, locale, exclude_fields=None):
|
||||
"""
|
||||
Creates a copy of this instance with the specified locale.
|
||||
|
||||
Note that the copy is initially unsaved.
|
||||
"""
|
||||
return CopyForTranslationAction(
|
||||
self,
|
||||
locale,
|
||||
exclude_fields=exclude_fields,
|
||||
).execute()
|
||||
|
||||
def get_default_locale(self):
|
||||
"""
|
||||
Finds the default locale to use for this object.
|
||||
|
||||
This will be called just before the initial save.
|
||||
"""
|
||||
# Check if the object has any parental keys to another translatable model
|
||||
# If so, take the locale from the object referenced in that parental key
|
||||
parental_keys = [
|
||||
field
|
||||
for field in self._meta.get_fields()
|
||||
if isinstance(field, ParentalKey)
|
||||
and issubclass(field.related_model, TranslatableMixin)
|
||||
]
|
||||
|
||||
if parental_keys:
|
||||
parent_id = parental_keys[0].value_from_object(self)
|
||||
return (
|
||||
parental_keys[0]
|
||||
.related_model.objects.defer()
|
||||
.select_related("locale")
|
||||
.get(id=parent_id)
|
||||
.locale
|
||||
)
|
||||
|
||||
return Locale.get_default()
|
||||
|
||||
@classmethod
|
||||
def get_translation_model(cls):
|
||||
"""
|
||||
Returns this model's "Translation model".
|
||||
|
||||
The "Translation model" is the model that has the ``locale`` and
|
||||
``translation_key`` fields.
|
||||
Typically this would be the current model, but it may be a
|
||||
super-class if multi-table inheritance is in use (as is the case
|
||||
for ``wagtailcore.Page``).
|
||||
"""
|
||||
return cls._meta.get_field("locale").model
|
||||
|
||||
|
||||
def bootstrap_translatable_model(model, locale):
|
||||
"""
|
||||
This function populates the "translation_key", and "locale" fields on model instances that were created
|
||||
before wagtail-localize was added to the site.
|
||||
|
||||
This can be called from a data migration, or instead you could use the "bootstrap_translatable_models"
|
||||
management command.
|
||||
"""
|
||||
for instance in (
|
||||
model.objects.filter(translation_key__isnull=True).defer().iterator()
|
||||
):
|
||||
instance.translation_key = uuid.uuid4()
|
||||
instance.locale = locale
|
||||
instance.save(update_fields=["translation_key", "locale"])
|
||||
|
||||
|
||||
class BootstrapTranslatableModel(migrations.RunPython):
|
||||
def __init__(self, model_string, language_code=None):
|
||||
if language_code is None:
|
||||
language_code = get_supported_content_language_variant(
|
||||
settings.LANGUAGE_CODE
|
||||
)
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
model = apps.get_model(model_string)
|
||||
Locale = apps.get_model("wagtailcore.Locale")
|
||||
|
||||
locale = Locale.objects.get(language_code=language_code)
|
||||
bootstrap_translatable_model(model, locale)
|
||||
|
||||
def backwards(apps, schema_editor):
|
||||
pass
|
||||
|
||||
super().__init__(forwards, backwards)
|
||||
|
||||
|
||||
class BootstrapTranslatableMixin(TranslatableMixin):
|
||||
"""
|
||||
A version of TranslatableMixin without uniqueness constraints.
|
||||
|
||||
This is to make it easy to transition existing models to being translatable.
|
||||
|
||||
The process is as follows:
|
||||
- Add BootstrapTranslatableMixin to the model
|
||||
- Run makemigrations
|
||||
- Create a data migration for each app, then use the BootstrapTranslatableModel operation in
|
||||
wagtail.models on each model in that app
|
||||
- Change BootstrapTranslatableMixin to TranslatableMixin
|
||||
- Run makemigrations again
|
||||
- Migrate!
|
||||
"""
|
||||
|
||||
translation_key = models.UUIDField(null=True, editable=False)
|
||||
locale = models.ForeignKey(
|
||||
Locale, on_delete=models.PROTECT, null=True, related_name="+", editable=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check(cls, **kwargs):
|
||||
# skip the check in TranslatableMixin that enforces the unique-together constraint
|
||||
return super(TranslatableMixin, cls).check(**kwargs)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def get_translatable_models(include_subclasses=False):
|
||||
"""
|
||||
Returns a list of all concrete models that inherit from TranslatableMixin.
|
||||
By default, this only includes models that are direct children of TranslatableMixin,
|
||||
to get all models, set the include_subclasses attribute to True.
|
||||
"""
|
||||
translatable_models = [
|
||||
model
|
||||
for model in apps.get_models()
|
||||
if issubclass(model, TranslatableMixin) and not model._meta.abstract
|
||||
]
|
||||
|
||||
if include_subclasses is False:
|
||||
# Exclude models that inherit from another translatable model
|
||||
root_translatable_models = set()
|
||||
|
||||
for model in translatable_models:
|
||||
root_translatable_models.add(model.get_translation_model())
|
||||
|
||||
translatable_models = [
|
||||
model for model in translatable_models if model in root_translatable_models
|
||||
]
|
||||
|
||||
return translatable_models
|
||||
|
||||
|
||||
@receiver(pre_save)
|
||||
def set_locale_on_new_instance(sender, instance, **kwargs):
|
||||
if not isinstance(instance, TranslatableMixin):
|
||||
return
|
||||
|
||||
if instance.locale_id is not None:
|
||||
return
|
||||
|
||||
# If this is a fixture load, use the global default Locale
|
||||
# as the page tree is probably in flux
|
||||
if kwargs["raw"]:
|
||||
instance.locale = Locale.get_default()
|
||||
return
|
||||
|
||||
instance.locale = instance.get_default_locale()
|
||||
Reference in New Issue
Block a user