mirror of
https://github.com/Hopiu/django-modeltranslation.git
synced 2026-05-27 13:34:00 +00:00
Add more control over required languages (close #143).
This commit is contained in:
parent
b55917303f
commit
591e945c33
6 changed files with 162 additions and 8 deletions
|
|
@ -154,15 +154,49 @@ involves copying migration files, using ``SOUTH_MIGRATION_MODULES`` setting,
|
|||
and passing ``--delete-ghost-migrations`` flag, so we don't recommend it.
|
||||
Invoking ``sync_translation_fields`` is plain easier.
|
||||
|
||||
Note that all added fields are
|
||||
Note that all added fields are by default
|
||||
declared ``blank=True`` and ``null=True`` no matter if the original field is
|
||||
required or not. In other words - all translations are optional. To populate
|
||||
the default translation fields added by the modeltranslation application
|
||||
required or not. In other words - all translations are optional, unless an explicit option
|
||||
is provided - see below.
|
||||
|
||||
To populate the default translation fields added by the modeltranslation application
|
||||
with values from existing database fields, you
|
||||
can use the ``update_translation_fields`` command below. See
|
||||
:ref:`commands-update_translation_fields` for more info on this.
|
||||
|
||||
|
||||
.. _required_langs:
|
||||
|
||||
Required fields
|
||||
---------------
|
||||
|
||||
By default, all translation fields are optional (not required). It can be changed using special
|
||||
attribute on ``TranslationOptions``, though::
|
||||
|
||||
class NewsTranslationOptions(TranslationOptions):
|
||||
fields = ('title', 'text',)
|
||||
required_languages = ('en', 'de')
|
||||
|
||||
It quite self-explanatory: for German and English, all translation fields are required. For other
|
||||
languages - optional.
|
||||
|
||||
A more fine-grained control is available::
|
||||
|
||||
class NewsTranslationOptions(TranslationOptions):
|
||||
fields = ('title', 'text',)
|
||||
required_languages = {'de': ('title', 'text'), 'default': ('title',)}
|
||||
|
||||
For German, all fields (both ``title`` and ``text``) are required; for all other languages - only
|
||||
``title`` is required. The ``'default'`` is optional.
|
||||
|
||||
.. note::
|
||||
Requirement is enforced by ``blank=False``. Please remember that it will trigger validation only
|
||||
in modelforms and admin (as always in Django). Manual model validation can be performed via
|
||||
``full_clean()`` model method.
|
||||
|
||||
The required fields are still ``null=True``, though.
|
||||
|
||||
|
||||
``TranslationOptions`` attributes reference
|
||||
-------------------------------------------
|
||||
|
||||
|
|
@ -205,6 +239,13 @@ Classes inheriting from ``TranslationOptions`` can have following attributes def
|
|||
empty_values = ''
|
||||
empty_values = {'title': '', 'slug': None, 'desc': 'both'}
|
||||
|
||||
.. attribute:: TranslationOptions.required_languages
|
||||
|
||||
Control which translation fields are required. See :ref:`required_langs`. ::
|
||||
|
||||
required_languages = ('en', 'de')
|
||||
required_languages = {'de': ('title','text'), 'default': ('title',)}
|
||||
|
||||
|
||||
.. _supported_field_matrix:
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ class TranslationField(object):
|
|||
that needs to be specified when the field is created.
|
||||
"""
|
||||
def __init__(self, translated_field, language, empty_value, *args, **kwargs):
|
||||
from modeltranslation.translator import translator
|
||||
|
||||
# Update the dict of this field with the content of the original one
|
||||
# This might be a bit radical?! Seems to work though...
|
||||
self.__dict__.update(translated_field.__dict__)
|
||||
|
|
@ -109,15 +111,35 @@ class TranslationField(object):
|
|||
if empty_value is NONE:
|
||||
self.empty_value = None if translated_field.null else ''
|
||||
|
||||
# Translation are always optional (for now - maybe add some parameters
|
||||
# to the translation options for configuring this)
|
||||
|
||||
# Default behaviour is that all translations are optional
|
||||
if not isinstance(self, fields.BooleanField):
|
||||
# TODO: Do we really want to enforce null *at all*? Shouldn't this
|
||||
# better honour the null setting of the translated field?
|
||||
self.null = True
|
||||
self.blank = True
|
||||
|
||||
# Take required_languages translation option into account
|
||||
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)):
|
||||
# All fields
|
||||
if self.language in required_languages:
|
||||
# self.null = False
|
||||
self.blank = False
|
||||
else:
|
||||
# Certain fields only
|
||||
# Try current language - if not present, try 'default' key
|
||||
try:
|
||||
req_fields = required_languages[self.language]
|
||||
except KeyError:
|
||||
req_fields = required_languages.get('default', ())
|
||||
if self.name in req_fields:
|
||||
# TODO: We might have to handle the whole thing through the
|
||||
# FieldsAggregationMetaClass, as fields can be inherited.
|
||||
# self.null = False
|
||||
self.blank = False
|
||||
|
||||
# Adjust the name of this field to reflect the language
|
||||
self.attname = build_localized_fieldname(self.translated_field.name, self.language)
|
||||
self.name = self.attname
|
||||
|
|
|
|||
|
|
@ -289,3 +289,12 @@ class CustomManager2(models.Manager):
|
|||
class CustomManager2TestModel(models.Model):
|
||||
title = models.CharField(ugettext_lazy('title'), max_length=255)
|
||||
objects = CustomManager2()
|
||||
|
||||
|
||||
########## Required fields testing
|
||||
|
||||
class RequiredModel(models.Model):
|
||||
non_req = models.CharField(max_length=10, blank=True)
|
||||
req = models.CharField(max_length=10)
|
||||
req_reg = models.CharField(max_length=10)
|
||||
req_en_reg = models.CharField(max_length=10)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ from modeltranslation.utils import (build_css_class, build_localized_fieldname,
|
|||
request = None
|
||||
|
||||
# How many models are registered for tests.
|
||||
TEST_MODELS = 27
|
||||
TEST_MODELS = 28
|
||||
|
||||
|
||||
class reload_override_settings(override_settings):
|
||||
|
|
@ -2709,3 +2709,46 @@ class ProxyModelTest(ModeltranslationTestBase):
|
|||
self.assertEqual(n.title, m.title)
|
||||
self.assertEqual(n.title_de, m.title_de)
|
||||
self.assertEqual(n.title_en, m.title_en)
|
||||
|
||||
|
||||
class TestRequired(ModeltranslationTestBase):
|
||||
def assertRequired(self, field_name):
|
||||
self.assertFalse(self.opts.get_field(field_name).blank)
|
||||
|
||||
def assertNotRequired(self, field_name):
|
||||
self.assertTrue(self.opts.get_field(field_name).blank)
|
||||
|
||||
def test_required(self):
|
||||
self.opts = models.RequiredModel._meta
|
||||
|
||||
# All non required
|
||||
self.assertNotRequired('non_req')
|
||||
self.assertNotRequired('non_req_en')
|
||||
self.assertNotRequired('non_req_de')
|
||||
|
||||
# Original required, but translated fields not - default behaviour
|
||||
self.assertRequired('req')
|
||||
self.assertNotRequired('req_en')
|
||||
self.assertNotRequired('req_de')
|
||||
|
||||
# Set all translated field required
|
||||
self.assertRequired('req_reg')
|
||||
self.assertRequired('req_reg_en')
|
||||
self.assertRequired('req_reg_de')
|
||||
|
||||
# Set some translated field required
|
||||
self.assertRequired('req_en_reg')
|
||||
self.assertRequired('req_en_reg_en')
|
||||
self.assertNotRequired('req_en_reg_de')
|
||||
|
||||
# Test validation
|
||||
inst = models.RequiredModel()
|
||||
inst.req = 'abc'
|
||||
inst.req_reg = 'def'
|
||||
try:
|
||||
inst.full_clean()
|
||||
except ValidationError as e:
|
||||
error_fields = set(e.message_dict.keys())
|
||||
self.assertEqual(set(('req_reg_en', 'req_en_reg', 'req_en_reg_en')), error_fields)
|
||||
else:
|
||||
self.fail('ValidationError not raised!')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ from modeltranslation.tests.models import (
|
|||
DescriptorModel, AbstractModelA, AbstractModelB, Slugged, MetaData, Displayable, Page,
|
||||
RichText, RichTextPage, MultitableModelA, MultitableModelB, MultitableModelC, ManagerTestModel,
|
||||
CustomManagerTestModel, CustomManager2TestModel, GroupFieldsetsModel, NameModel,
|
||||
ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel)
|
||||
ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel,
|
||||
RequiredModel)
|
||||
|
||||
|
||||
class TestTranslationOptions(TranslationOptions):
|
||||
|
|
@ -186,3 +187,14 @@ translator.register(GroupFieldsetsModel, GroupFieldsetsTranslationOptions)
|
|||
class NameTranslationOptions(TranslationOptions):
|
||||
fields = ('firstname', 'lastname', 'slug2')
|
||||
translator.register(NameModel, NameTranslationOptions)
|
||||
|
||||
|
||||
########## Required fields testing
|
||||
|
||||
class RequiredTranslationOptions(TranslationOptions):
|
||||
fields = ('non_req', 'req', 'req_reg', 'req_en_reg')
|
||||
required_languages = {
|
||||
'en': ('req_reg', 'req_en_reg',),
|
||||
'default': ('req_reg',), # for all other languages
|
||||
}
|
||||
translator.register(RequiredModel, RequiredTranslationOptions)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.utils.six import with_metaclass
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import Manager, ForeignKey, OneToOneField
|
||||
from django.db.models.base import ModelBase
|
||||
from django.db.models.signals import post_init
|
||||
|
|
@ -57,6 +58,7 @@ class TranslationOptions(with_metaclass(FieldsAggregationMetaClass, object)):
|
|||
with translated model. This model may be not translated itself.
|
||||
``related_fields`` contains names of reverse lookup fields.
|
||||
"""
|
||||
required_languages = ()
|
||||
|
||||
def __init__(self, model):
|
||||
"""
|
||||
|
|
@ -69,6 +71,28 @@ class TranslationOptions(with_metaclass(FieldsAggregationMetaClass, object)):
|
|||
self.fields = dict((f, set()) for f in self.fields)
|
||||
self.related_fields = []
|
||||
|
||||
def validate(self):
|
||||
"""
|
||||
Perform options validation.
|
||||
"""
|
||||
# TODO: at the moment only required_languages is validated.
|
||||
# Maybe check other options as well?
|
||||
if self.required_languages:
|
||||
if isinstance(self.required_languages, (tuple, list)):
|
||||
self._check_languages(self.required_languages)
|
||||
else:
|
||||
self._check_languages(self.required_languages.iterkeys(), extra=('default',))
|
||||
for fieldnames in self.required_languages.itervalues():
|
||||
if any(f not in self.fields for f in fieldnames):
|
||||
raise ImproperlyConfigured(
|
||||
'Fieldname in required_languages which is not in fields option.')
|
||||
|
||||
def _check_languages(self, languages, extra=()):
|
||||
correct = mt_settings.AVAILABLE_LANGUAGES + list(extra)
|
||||
if any(l not in correct for l in languages):
|
||||
raise ImproperlyConfigured(
|
||||
'Language in required_languages which is not in AVAILABLE_LANGUAGES.')
|
||||
|
||||
def update(self, other):
|
||||
"""
|
||||
Update with options from a superclass.
|
||||
|
|
@ -342,6 +366,9 @@ class Translator(object):
|
|||
# Find inherited fields and create options instance for the model.
|
||||
opts = self._get_options_for_model(model, opts_class, **options)
|
||||
|
||||
# Now, when all fields are initialized and inherited, validate configuration.
|
||||
opts.validate()
|
||||
|
||||
# Mark the object explicitly as registered -- registry caches
|
||||
# options of all models, registered or not.
|
||||
opts.registered = True
|
||||
|
|
|
|||
Loading…
Reference in a new issue