Source code for utils.translation
"""
This module makes it easy to define translatable model fields.
To use it in a ``models.py``, make sure you;
- set the metaclass of your model to :class:`ModelTranslateMeta`
- replace any translatable model fields by :class:`MultilingualField`
instances
- make and apply database migrations
See the following usage example;
.. code:: python
from django.db import models
from utils.translation import MultilingualField, ModelTranslateMeta
class SomeItem(models.Model, metaclass=ModelTranslateMeta):
name = MultilingualField(models.CharField, max_length=100)
description = MultilingualField(models.TextField)
In order to use the fields in :class:`~django.contrib.admin.ModelAdmin`
configuration (such as in the ``fields``, ``fieldsets`` or
``prepopulated_fields`` attributes), subclass :class:`TranslatedModelAdmin`
instead;
.. code:: python
from utils.translation import TranslatedModelAdmin
class SomeItemAdmin(TranslatedModelAdmin):
fields = (name, description)
"""
from django.conf import settings
from django.contrib import admin
from django.core.exceptions import FieldError, ImproperlyConfigured
from django.db import models
from django.db.models.fields.related import RelatedField
from django.utils.text import format_lazy
from django.utils.translation import get_language
[docs]class MultilingualField:
"""
Transformed the passed-in form field into fields appended with the
active languages and generates an automatic accessor property that
translates based on the currently active language
Requires a :class:`~django.db.models.Model` metaclassed by
:class:`ModelTranslateMeta`.
"""
def __init__(self, cls, *args, **kwargs):
"""Construct the MultilingualField
:param cls: the form field to instantiate.
Any additional arguments are passed to the field.
"""
if issubclass(cls, RelatedField):
# Especially naming the reverses gets quite messy for these.
# TODO consider implementing this when there is a need for it.
raise NotImplementedError("RelatedFields are not translatable.")
if get_language() is None:
raise ImproperlyConfigured("I18n does not appear to be activated.")
self.cls = cls
self.args = args
self.kwargs = kwargs
[docs]def localize_attr_name(attr_name, language=None):
"""Generate the localized attribute name"""
if language is None:
language = get_language()
if language is None:
language = settings.LANGUAGE_CODE
return "{}_{}".format(attr_name, language)
def _i18n_attr_accessor(attr):
def _accessor(self):
return getattr(self, localize_attr_name(attr))
_accessor.__doc__ = "Accessor that fetches the localized variant of {}".format(attr)
return _accessor
[docs]class TranslatedModelAdmin(admin.ModelAdmin):
"""
This class should be used when :class:`~django.contrib.admin.ModelAdmin`
is used with a translated model and one refers to such a field in the
``fields`` or ``fieldsets`` attributes, or in ``prepopulated_fields``.
This works because :class:`~django.contrib.admin.ModelAdmin` has an empty
metaclass; we can hook in to ``__init__`` and modify the attributes
when ``model`` is known.
"""
def __init__(self, model, admin_site):
for key, fields in list(type(self).prepopulated_fields.items()):
# Replace translated fields in `fields`
fields = tuple(
model._meta._field_i18n["default"].get(field, field) for field in fields
)
# ..and in `key`
del type(self).prepopulated_fields[key]
key = model._meta._field_i18n["default"].get(key, key)
type(self).prepopulated_fields[key] = fields
def _trans_fields(fields):
if fields is None:
return None
fields = [
model._meta._field_i18n["fields"].get(field, (field,))
for field in fields
]
return tuple(field for fieldset in fields for field in fieldset)
# In fields, we replace a translated field by all resulting fields.
type(self).fields = _trans_fields(type(self).fields)
type(self).exclude = _trans_fields(type(self).exclude)
type(self).search_fields = _trans_fields(type(self).search_fields)
if type(self).fieldsets is not None:
for fieldset in type(self).fieldsets:
fieldset[1]["fields"] = _trans_fields(fieldset[1]["fields"])
super().__init__(model, admin_site)