Add more control over required languages (close #143).

This commit is contained in:
Dirk Eschler 2013-02-18 17:45:36 +01:00 committed by Jacek Tomaszewski
parent b55917303f
commit 591e945c33
6 changed files with 162 additions and 8 deletions

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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!')

View file

@ -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)

View file

@ -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