mirror of
https://github.com/Hopiu/django-modeltranslation.git
synced 2026-05-19 18:01:54 +00:00
Merge pull request #137 from wrwrwr/feature/translation-fields-inheritance
Translation fields inheritance
This commit is contained in:
commit
a78639ad63
9 changed files with 330 additions and 183 deletions
|
|
@ -47,10 +47,12 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
|
||||
# For every localized field copy the widget from the original field
|
||||
# and add a css class to identify a modeltranslation widget.
|
||||
if db_field.name in self.trans_opts.localized_fieldnames_rev:
|
||||
orig_fieldname = self.trans_opts.localized_fieldnames_rev[db_field.name]
|
||||
orig_formfield = self.formfield_for_dbfield(
|
||||
self.model._meta.get_field(orig_fieldname), **kwargs)
|
||||
try:
|
||||
orig_field = db_field.translated_field
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
orig_formfield = self.formfield_for_dbfield(orig_field, **kwargs)
|
||||
field.widget = deepcopy(orig_formfield.widget)
|
||||
css_classes = field.widget.attrs.get('class', '').split(' ')
|
||||
css_classes.append('mt')
|
||||
|
|
@ -62,7 +64,7 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
# widget.
|
||||
css_classes.append('mt-default')
|
||||
if (orig_formfield.required or self._orig_was_required.get(
|
||||
'%s.%s' % (db_field.model._meta, orig_fieldname))):
|
||||
'%s.%s' % (orig_field.model._meta, orig_field.name))):
|
||||
# In case the original form field was required, make the
|
||||
# default translation field required instead.
|
||||
orig_formfield.required = False
|
||||
|
|
@ -76,8 +78,8 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
exclude = tuple()
|
||||
if exclude:
|
||||
exclude_new = tuple(exclude)
|
||||
return exclude_new + tuple(self.trans_opts.fields)
|
||||
return tuple(self.trans_opts.fields)
|
||||
return exclude_new + tuple(self.trans_opts.fields.keys())
|
||||
return tuple(self.trans_opts.fields.keys())
|
||||
|
||||
def replace_orig_field(self, option):
|
||||
"""
|
||||
|
|
@ -87,9 +89,9 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
Returns a new list with replaced fields. If `option` contains no
|
||||
registered fields, it is returned unmodified.
|
||||
|
||||
>>> print self.trans_opts.fields
|
||||
('title',)
|
||||
>>> get_translation_fields(self.trans_opts.fields[0])
|
||||
>>> print self.trans_opts.fields.keys()
|
||||
['title',]
|
||||
>>> get_translation_fields(self.trans_opts.fields.keys()[0])
|
||||
['title_de', 'title_en']
|
||||
>>> self.replace_orig_field(['title', 'url'])
|
||||
['title_de', 'title_en', 'url']
|
||||
|
|
@ -100,10 +102,6 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
2. They don't scale well with more than a few languages
|
||||
3. It's better than not handling them at all (okay that's weak)
|
||||
|
||||
>>> print self.trans_opts.fields
|
||||
(('title', 'url'), 'email')
|
||||
>>> get_translation_fields(self.trans_opts.fields[0])
|
||||
['title_de', 'title_en', 'url_de', 'url_en', 'email_de', 'email_en']
|
||||
>>> self.replace_orig_field((('title', 'url'), 'email', 'text'))
|
||||
['title_de', 'title_en', 'url_de', 'url_en', 'email_de', 'email_en', 'text']
|
||||
"""
|
||||
|
|
@ -187,9 +185,9 @@ class TranslationBaseModelAdmin(BaseModelAdmin):
|
|||
if exclude_languages:
|
||||
excl_languages = exclude_languages
|
||||
exclude = []
|
||||
for orig_fieldname, translation_fields in self.trans_opts.localized_fieldnames.iteritems():
|
||||
for orig_fieldname, translation_fields in self.trans_opts.fields.iteritems():
|
||||
for tfield in translation_fields:
|
||||
language = tfield.split('_')[-1]
|
||||
language = tfield.name.split('_')[-1]
|
||||
if language in excl_languages and tfield not in exclude:
|
||||
exclude.append(tfield)
|
||||
return tuple(exclude)
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ You will need to execute this command in two cases:
|
|||
Credits: Heavily inspired by django-transmeta's sync_transmeta_db command.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from django.core.management.color import no_style
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import get_models
|
||||
|
||||
from modeltranslation.translator import translator, NotRegistered
|
||||
from modeltranslation.translator import translator
|
||||
from modeltranslation.utils import build_localized_fieldname
|
||||
|
||||
|
||||
|
|
@ -41,46 +40,38 @@ def print_missing_langs(missing_langs, field_name, model_name):
|
|||
field_name, model_name, ", ".join(missing_langs))
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Detect new translatable fields or new available languages and sync database structure'
|
||||
class Command(NoArgsCommand):
|
||||
help = ('Detect new translatable fields or new available languages and'
|
||||
' sync database structure. Does not remove columns of removed'
|
||||
' languages or undeclared fields.')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
def handle_noargs(self, **options):
|
||||
"""
|
||||
Command execution.
|
||||
"""
|
||||
self.cursor = connection.cursor()
|
||||
self.introspection = connection.introspection
|
||||
|
||||
all_models = get_models()
|
||||
found_missing_fields = False
|
||||
for model in all_models:
|
||||
try:
|
||||
options = translator.get_options_for_model(model)
|
||||
# Options returns full-wide spectrum of localized fields but
|
||||
# we only want to synchronize the local fields attached to the
|
||||
# model.
|
||||
local_field_names = [field.name for field in model._meta.local_fields]
|
||||
translatable_fields = [field for field in options.localized_fieldnames
|
||||
if field in local_field_names]
|
||||
model_full_name = '%s.%s' % (model._meta.app_label, model._meta.module_name)
|
||||
db_table = model._meta.db_table
|
||||
for field_name in translatable_fields:
|
||||
missing_langs = list(
|
||||
self.get_missing_languages(field_name, db_table))
|
||||
if missing_langs:
|
||||
found_missing_fields = True
|
||||
print_missing_langs(missing_langs, field_name, model_full_name)
|
||||
sql_sentences = self.get_sync_sql(field_name, missing_langs, model)
|
||||
execute_sql = ask_for_confirmation(sql_sentences, model_full_name)
|
||||
if execute_sql:
|
||||
print 'Executing SQL...',
|
||||
for sentence in sql_sentences:
|
||||
self.cursor.execute(sentence)
|
||||
print 'Done'
|
||||
else:
|
||||
print 'SQL not executed'
|
||||
except NotRegistered:
|
||||
pass
|
||||
models = translator.get_registered_models(abstract=False)
|
||||
for model in models:
|
||||
db_table = model._meta.db_table
|
||||
model_full_name = '%s.%s' % (model._meta.app_label, model._meta.module_name)
|
||||
opts = translator.get_options_for_model(model)
|
||||
for field_name in opts.local_fields.iterkeys():
|
||||
missing_langs = list(self.get_missing_languages(field_name, db_table))
|
||||
if missing_langs:
|
||||
found_missing_fields = True
|
||||
print_missing_langs(missing_langs, field_name, model_full_name)
|
||||
sql_sentences = self.get_sync_sql(field_name, missing_langs, model)
|
||||
execute_sql = ask_for_confirmation(sql_sentences, model_full_name)
|
||||
if execute_sql:
|
||||
print 'Executing SQL...',
|
||||
for sentence in sql_sentences:
|
||||
self.cursor.execute(sentence)
|
||||
print 'Done'
|
||||
else:
|
||||
print 'SQL not executed'
|
||||
|
||||
transaction.commit_unless_managed()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,25 +8,26 @@ from modeltranslation.utils import build_localized_fieldname
|
|||
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = ('Updates the default translation fields of all or the specified '
|
||||
'translated application using the value of the original field.')
|
||||
help = ('Updates empty values of default translation fields using'
|
||||
' values from original fields (in all translated models).')
|
||||
|
||||
def handle(self, **options):
|
||||
def handle_noargs(self, **options):
|
||||
verbosity = int(options['verbosity'])
|
||||
if verbosity > 0:
|
||||
self.stdout.write("Using default language: %s\n" % DEFAULT_LANGUAGE)
|
||||
for model, trans_opts in translator._registry.items():
|
||||
if model._meta.abstract:
|
||||
continue
|
||||
models = translator.get_registered_models(abstract=False)
|
||||
for model in models:
|
||||
if verbosity > 0:
|
||||
self.stdout.write("Updating data of model '%s'\n" % model)
|
||||
for fieldname in trans_opts.fields:
|
||||
def_lang_fieldname = build_localized_fieldname(fieldname, DEFAULT_LANGUAGE)
|
||||
opts = translator.get_options_for_model(model)
|
||||
for field_name in opts.fields.iterkeys():
|
||||
def_lang_fieldname = build_localized_fieldname(field_name, DEFAULT_LANGUAGE)
|
||||
|
||||
# We'll only update fields which do not have an existing value
|
||||
q = Q(**{def_lang_fieldname: None})
|
||||
field = model._meta.get_field(fieldname)
|
||||
field = model._meta.get_field(field_name)
|
||||
if field.empty_strings_allowed:
|
||||
q |= Q(**{def_lang_fieldname: ""})
|
||||
|
||||
model.objects.filter(q).rewrite(False).update(**{def_lang_fieldname: F(fieldname)})
|
||||
model.objects.filter(q).rewrite(False).update(
|
||||
**{def_lang_fieldname: F(field_name)})
|
||||
|
|
|
|||
|
|
@ -14,18 +14,12 @@ from modeltranslation import settings
|
|||
from modeltranslation.utils import build_localized_fieldname, get_language
|
||||
|
||||
|
||||
_registry = {}
|
||||
|
||||
|
||||
def get_translatable_fields_for_model(model):
|
||||
from modeltranslation import translator
|
||||
if model not in _registry:
|
||||
try:
|
||||
_registry[model] = dict(
|
||||
translator.translator.get_options_for_model(model).localized_fieldnames)
|
||||
except translator.NotRegistered:
|
||||
_registry[model] = None
|
||||
return _registry[model]
|
||||
from modeltranslation.translator import NotRegistered, translator
|
||||
try:
|
||||
return translator.get_options_for_model(model).fields
|
||||
except NotRegistered:
|
||||
return None
|
||||
|
||||
|
||||
def rewrite_lookup_key(model, lookup_key):
|
||||
|
|
@ -66,7 +60,7 @@ def rewrite_order_lookup_key(model, lookup_key):
|
|||
def get_fields_to_translatable_models(model):
|
||||
from modeltranslation.translator import translator
|
||||
results = []
|
||||
for field_name in translator.get_options_for_model(model).localized_fieldnames.keys():
|
||||
for field_name in translator.get_options_for_model(model).fields.keys():
|
||||
field_object, modelclass, direct, m2m = model._meta.get_field_by_name(field_name)
|
||||
if direct and isinstance(field_object, RelatedField):
|
||||
if get_translatable_fields_for_model(field_object.related.parent_model) is not None:
|
||||
|
|
@ -188,8 +182,8 @@ class MultilingualQuerySet(models.query.QuerySet):
|
|||
for key, val in kwargs.items():
|
||||
if key in translatable_fields:
|
||||
# Try to add value in every language
|
||||
for new_key in translatable_fields[key]:
|
||||
kwargs.setdefault(new_key, val)
|
||||
for translation_field in translatable_fields[key]:
|
||||
kwargs.setdefault(translation_field.name, val)
|
||||
# If not use populate feature, then normal rewriting will occur at model's __init__
|
||||
# That's why it is not performed here - no reason to rewrite twice.
|
||||
return super(MultilingualQuerySet, self).create(**kwargs)
|
||||
|
|
|
|||
|
|
@ -39,14 +39,15 @@ def autodiscover():
|
|||
import_module(module)
|
||||
|
||||
# In debug mode, print a list of registered models and pid to stdout.
|
||||
# Note: Differing model order is fine, _registry is just a dict and we
|
||||
# don't rely on a particular order.
|
||||
# Note: Differing model order is fine, we don't rely on a particular
|
||||
# order, as far as base classes are registered before subclasses.
|
||||
if DEBUG:
|
||||
try:
|
||||
if sys.argv[1] in ('runserver', 'runserver_plus'):
|
||||
translated_model_names = ', '.join(t.__name__ for t in translator._registry.keys())
|
||||
print('modeltranslation: Registered %d models for translation (%s) [pid:%d].' % (
|
||||
len(translator._registry), translated_model_names, os.getpid()))
|
||||
models = translator.get_registered_models()
|
||||
names = ', '.join(m.__name__ for m in models)
|
||||
print('modeltranslation: Registered %d models for translation'
|
||||
' (%s) [pid: %d].' % (len(models), names, os.getpid()))
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ from modeltranslation.tests.translation import (FallbackModel2TranslationOptions
|
|||
FieldInheritanceCTranslationOptions,
|
||||
FieldInheritanceETranslationOptions)
|
||||
from modeltranslation.tests.test_settings import TEST_SETTINGS
|
||||
from modeltranslation.utils import build_css_class
|
||||
from modeltranslation.utils import build_css_class, build_localized_fieldname
|
||||
|
||||
try:
|
||||
from django.test.utils import override_settings
|
||||
|
|
@ -227,16 +227,24 @@ class ModeltranslationTest(ModeltranslationTestBase):
|
|||
self.failUnless(translator.translator)
|
||||
|
||||
# Check that all models are registered for translation
|
||||
self.failUnlessEqual(len(translator.translator._registry), 13)
|
||||
self.assertEqual(len(translator.translator.get_registered_models()), 19)
|
||||
|
||||
# Try to unregister a model that is not registered
|
||||
self.assertRaises(translator.NotRegistered,
|
||||
translator.translator.unregister, User)
|
||||
translator.translator.unregister, models.BasePage)
|
||||
|
||||
# Try to get options for a model that is not registered
|
||||
self.assertRaises(translator.NotRegistered,
|
||||
translator.translator.get_options_for_model, User)
|
||||
|
||||
# Ensure that a base can't be registered after a subclass.
|
||||
self.assertRaises(translator.DescendantRegistered,
|
||||
translator.translator.register, models.BasePage)
|
||||
|
||||
# Or unregistered before it.
|
||||
self.assertRaises(translator.DescendantRegistered,
|
||||
translator.translator.unregister, models.Slugged)
|
||||
|
||||
def test_fields(self):
|
||||
field_names = dir(models.TestModel())
|
||||
self.failUnless('id' in field_names)
|
||||
|
|
@ -400,11 +408,11 @@ class ModeltranslationTest(ModeltranslationTestBase):
|
|||
def _test_constructor(self, keywords):
|
||||
n = models.TestModel(**keywords)
|
||||
m = models.TestModel.objects.create(**keywords)
|
||||
fields = translator.translator.get_options_for_model(models.TestModel).localized_fieldnames
|
||||
for base_field, trans_fields in fields.iteritems():
|
||||
opts = translator.translator.get_options_for_model(models.TestModel)
|
||||
for base_field, trans_fields in opts.fields.iteritems():
|
||||
self._compare_instances(n, m, base_field)
|
||||
for lang_field in trans_fields:
|
||||
self._compare_instances(n, m, lang_field)
|
||||
self._compare_instances(n, m, lang_field.name)
|
||||
|
||||
def test_constructor(self):
|
||||
"""
|
||||
|
|
@ -1213,6 +1221,45 @@ class ModelInheritanceTest(ModeltranslationTestBase):
|
|||
self.failUnless('titleb_en' in field_names_d)
|
||||
self.failUnless('titled' in field_names_d)
|
||||
|
||||
def test_inheritance(self):
|
||||
def assertLocalFields(model, local_fields):
|
||||
# Proper fields are inherited.
|
||||
opts = translator.translator.get_options_for_model(model)
|
||||
self.assertEqual(set(opts.local_fields.keys()), set(local_fields))
|
||||
# Local translation fields are created on the model.
|
||||
model_local_fields = [f.name for f in model._meta.local_fields]
|
||||
for field in local_fields:
|
||||
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
||||
translation_field = build_localized_fieldname(field, lang)
|
||||
self.assertTrue(translation_field in model_local_fields)
|
||||
|
||||
def assertFields(model, fields):
|
||||
# The given fields are inherited.
|
||||
opts = translator.translator.get_options_for_model(model)
|
||||
self.assertEqual(set(opts.fields.keys()), set(fields))
|
||||
# Inherited translation fields are available on the model.
|
||||
model_fields = model._meta.get_all_field_names()
|
||||
for field in fields:
|
||||
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
||||
translation_field = build_localized_fieldname(field, lang)
|
||||
self.assertTrue(translation_field in model_fields)
|
||||
|
||||
# Translation fields can be declared on abstract classes.
|
||||
assertLocalFields(models.Slugged, ('slug',))
|
||||
assertLocalFields(models.MetaData, ('keywords',))
|
||||
assertLocalFields(models.RichText, ('content',))
|
||||
# Local fields are inherited from abstract superclasses.
|
||||
assertLocalFields(models.Displayable, ('slug', 'keywords',))
|
||||
assertLocalFields(models.Page, ('slug', 'keywords', 'title',))
|
||||
# But not from concrete superclasses.
|
||||
assertLocalFields(models.RichTextPage, ('content',))
|
||||
|
||||
# Fields inherited from concrete models are also available.
|
||||
assertFields(models.Slugged, ('slug',))
|
||||
assertFields(models.Page, ('slug', 'keywords', 'title',))
|
||||
assertFields(models.RichTextPage, ('slug', 'keywords', 'title',
|
||||
'content',))
|
||||
|
||||
|
||||
class ModelInheritanceFieldAggregationTest(ModeltranslationTestBase):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -87,6 +87,47 @@ class AbstractModelB(AbstractModelA):
|
|||
titleb = models.CharField(ugettext_lazy('title b'), max_length=255)
|
||||
|
||||
|
||||
########## Fields inheritance testing
|
||||
|
||||
class Slugged(models.Model):
|
||||
slug = models.CharField(max_length=255)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class MetaData(models.Model):
|
||||
keywords = models.CharField(max_length=255)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Displayable(Slugged, MetaData):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class BasePage(Displayable):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Page(BasePage):
|
||||
title = models.CharField(max_length=255)
|
||||
|
||||
|
||||
class RichText(models.Model):
|
||||
content = models.CharField(max_length=255)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class RichTextPage(Page, RichText):
|
||||
pass
|
||||
|
||||
|
||||
########## Admin testing
|
||||
|
||||
class DataModel(models.Model):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from modeltranslation.translator import translator, TranslationOptions
|
|||
from modeltranslation.tests.models import (
|
||||
TestModel, FallbackModel, FallbackModel2,
|
||||
FileFieldsModel, OtherFieldsModel, AbstractModelA, AbstractModelB,
|
||||
Slugged, MetaData, Displayable, Page, RichText, RichTextPage,
|
||||
MultitableModelA, MultitableBModelA, MultitableModelC,
|
||||
ManagerTestModel, CustomManagerTestModel, CustomManager2TestModel)
|
||||
|
||||
|
|
@ -75,6 +76,33 @@ class AbstractModelBTranslationOptions(TranslationOptions):
|
|||
translator.register(AbstractModelB, AbstractModelBTranslationOptions)
|
||||
|
||||
|
||||
########## Fields inheritance testing
|
||||
|
||||
class SluggedTranslationOptions(TranslationOptions):
|
||||
fields = ('slug',)
|
||||
|
||||
|
||||
class MetaDataTranslationOptions(TranslationOptions):
|
||||
fields = ('keywords',)
|
||||
|
||||
|
||||
class RichTextTranslationOptions(TranslationOptions):
|
||||
fields = ('content',)
|
||||
|
||||
|
||||
class PageTranslationOptions(TranslationOptions):
|
||||
fields = ('title',)
|
||||
|
||||
|
||||
# BasePage left unregistered intentionally.
|
||||
translator.register(Slugged, SluggedTranslationOptions)
|
||||
translator.register(MetaData, MetaDataTranslationOptions)
|
||||
translator.register(RichText, RichTextTranslationOptions)
|
||||
translator.register(Displayable)
|
||||
translator.register(Page, PageTranslationOptions)
|
||||
translator.register(RichTextPage)
|
||||
|
||||
|
||||
########## Manager testing
|
||||
|
||||
class ManagerTestModelTranslationOptions(TranslationOptions):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from django.db.models.base import ModelBase
|
|||
|
||||
from modeltranslation.fields import TranslationFieldDescriptor, create_translation_field
|
||||
from modeltranslation.manager import MultilingualManager, rewrite_lookup_key
|
||||
from modeltranslation.utils import build_localized_fieldname, unique
|
||||
from modeltranslation.utils import build_localized_fieldname
|
||||
|
||||
|
||||
class AlreadyRegistered(Exception):
|
||||
|
|
@ -16,51 +16,79 @@ class NotRegistered(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class DescendantRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FieldsAggregationMetaClass(type):
|
||||
"""
|
||||
Metaclass to handle inheritance of fields between classes.
|
||||
Metaclass to handle custom inheritance of fields between classes.
|
||||
"""
|
||||
def __new__(cls, name, bases, attrs):
|
||||
parents = [b for b in bases if isinstance(b, FieldsAggregationMetaClass)]
|
||||
if not parents:
|
||||
return super(FieldsAggregationMetaClass, cls).__new__(cls, name, bases, attrs)
|
||||
attrs['fields'] = tuple(attrs.get('fields', ()))
|
||||
for base in parents:
|
||||
attrs['fields'] += tuple(base.fields)
|
||||
attrs['fields'] = tuple(unique(attrs['fields']))
|
||||
attrs['fields'] = set(attrs.get('fields', ()))
|
||||
for base in bases:
|
||||
if isinstance(base, FieldsAggregationMetaClass):
|
||||
attrs['fields'].update(base.fields)
|
||||
attrs['fields'] = tuple(attrs['fields'])
|
||||
return super(FieldsAggregationMetaClass, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
class TranslationOptions(object):
|
||||
"""
|
||||
The TranslationOptions object is used to specify the fields to translate.
|
||||
Translatable fields are declared by registering a model using
|
||||
``TranslationOptions`` class with appropriate ``fields`` attribute.
|
||||
Model-specific fallback values and languages can also be given as class
|
||||
attributes.
|
||||
|
||||
The options are registered in combination with a model class at the
|
||||
``modeltranslation.translator.translator`` instance.
|
||||
|
||||
It caches the content type of the translated model for faster lookup later
|
||||
on.
|
||||
Options instances hold info about translatable fields for a model and its
|
||||
superclasses. The ``local_fields`` and ``fields`` attributes are mappings
|
||||
from fields to sets of their translation fields; ``local_fields`` contains
|
||||
only those fields that are handled in the model's database table (those
|
||||
inherited from abstract superclasses, unless there is a concrete superclass
|
||||
in between in the inheritance chain), while ``fields`` also includes fields
|
||||
inherited from concrete supermodels (giving all translated fields available
|
||||
on a model).
|
||||
"""
|
||||
__metaclass__ = FieldsAggregationMetaClass
|
||||
fields = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.localized_fieldnames = []
|
||||
def __init__(self, model):
|
||||
"""
|
||||
Create fields dicts without any translation fields.
|
||||
"""
|
||||
self.model = model
|
||||
self.registered = False
|
||||
self.local_fields = dict((f, set()) for f in self.fields)
|
||||
self.fields = dict((f, set()) for f in self.fields)
|
||||
|
||||
def update(self, other):
|
||||
"""
|
||||
Update with options from a superclass.
|
||||
"""
|
||||
if other.model._meta.abstract:
|
||||
self.local_fields.update(other.local_fields)
|
||||
self.fields.update(other.fields)
|
||||
|
||||
def add_translation_field(self, field, 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 __str__(self):
|
||||
local = tuple(self.local_fields.keys())
|
||||
inherited = tuple(set(self.fields.keys()) - set(local))
|
||||
return '%s: %s + %s' % (self.__class__.__name__, local, inherited)
|
||||
|
||||
|
||||
def add_localized_fields(model):
|
||||
def add_translation_fields(model, opts):
|
||||
"""
|
||||
Monkey patches the original model class to provide additional fields for
|
||||
every language. Only do that for fields which are defined in the
|
||||
translation options of the model.
|
||||
every language.
|
||||
|
||||
Returns a dict mapping the original fieldname to a list containing the
|
||||
names of the localized fields created for the original field.
|
||||
Adds newly created translation fields to the given translation options.
|
||||
"""
|
||||
localized_fields = dict()
|
||||
translation_opts = translator.get_options_for_model(model)
|
||||
for field_name in translation_opts.fields:
|
||||
localized_fields[field_name] = list()
|
||||
for field_name in opts.local_fields.iterkeys():
|
||||
for l in settings.LANGUAGES:
|
||||
# Create a dynamic translation field
|
||||
translation_field = create_translation_field(
|
||||
|
|
@ -75,8 +103,7 @@ def add_localized_fields(model):
|
|||
# This approach implements the translation fields as full valid
|
||||
# django model fields and therefore adds them via add_to_class
|
||||
model.add_to_class(localized_field_name, translation_field)
|
||||
localized_fields[field_name].append(localized_field_name)
|
||||
return localized_fields
|
||||
opts.add_translation_field(field_name, translation_field)
|
||||
|
||||
|
||||
def add_manager(model):
|
||||
|
|
@ -127,7 +154,7 @@ def patch_constructor(model):
|
|||
#def translated_model_initializing(sender, args, kwargs, **signal_kwargs):
|
||||
#print "translated_model_initializing", sender, args, kwargs
|
||||
#trans_opts = translator.get_options_for_model(sender)
|
||||
#for field_name in trans_opts.fields:
|
||||
#for field_name in trans_opts.local_fields:
|
||||
#setattr(sender, field_name, TranslationFieldDescriptor(field_name))
|
||||
|
||||
|
||||
|
|
@ -153,51 +180,47 @@ class Translator(object):
|
|||
registered with the Translator using the register() method.
|
||||
"""
|
||||
def __init__(self):
|
||||
# model_class class -> translation_opts instance
|
||||
# All seen models (model class -> ``TranslationOptions`` instance).
|
||||
self._registry = {}
|
||||
|
||||
def register(self, model_or_iterable, translation_opts, **options):
|
||||
def register(self, model_or_iterable, opts_class=None, **options):
|
||||
"""
|
||||
Registers the given model(s) with the given translation options.
|
||||
|
||||
The model(s) should be Model classes, not instances.
|
||||
|
||||
If a model is already registered for translation, this will raise
|
||||
AlreadyRegistered.
|
||||
Fields declared for translation on a base class are inherited by
|
||||
subclasses. If the model or one of its subclasses is already
|
||||
registered for translation, this will raise an exception.
|
||||
"""
|
||||
if isinstance(model_or_iterable, ModelBase):
|
||||
model_or_iterable = [model_or_iterable]
|
||||
|
||||
for model in model_or_iterable:
|
||||
# Ensure that a base is not registered after a subclass (_registry
|
||||
# is closed with respect to taking bases, so we can just check if
|
||||
# we've seen the model).
|
||||
if model in self._registry:
|
||||
raise AlreadyRegistered(
|
||||
'The model %s is already registered for translation' % model.__name__)
|
||||
if self._registry[model].registered:
|
||||
raise AlreadyRegistered(
|
||||
'Model "%s" is already registered for translation' %
|
||||
model.__name__)
|
||||
else:
|
||||
descendants = [d.__name__ for d in self._registry.keys()
|
||||
if issubclass(d, model) and d != model]
|
||||
raise DescendantRegistered(
|
||||
'Model "%s" cannot be registered after its subclass'
|
||||
' "%s"' % (model.__name__, descendants[0]))
|
||||
|
||||
# If we got **options then dynamically construct a subclass of
|
||||
# translation_opts with those **options.
|
||||
if options:
|
||||
# For reasons I don't quite understand, without a __module__
|
||||
# the created class appears to "live" in the wrong place,
|
||||
# which causes issues later on.
|
||||
options['__module__'] = __name__
|
||||
translation_opts = type(
|
||||
"%sTranslationOptions" % model.__name__, (translation_opts,), options)
|
||||
# Find inherited fields and create options instance for the model.
|
||||
opts = self._get_options_for_model(model, opts_class, **options)
|
||||
|
||||
# Store the translation class associated to the model
|
||||
self._registry[model] = translation_opts
|
||||
# Mark the object explicitly as registered -- registry caches
|
||||
# options of all models, registered or not.
|
||||
opts.registered = True
|
||||
|
||||
# Add the localized fields to the model and store the names of
|
||||
# these fields in the model's translation options for faster lookup
|
||||
# later on.
|
||||
translation_opts.localized_fieldnames = add_localized_fields(model)
|
||||
|
||||
# Create a reverse dict mapping the localized_fieldnames to the
|
||||
# original fieldname
|
||||
rev_dict = dict()
|
||||
for orig_name, loc_names in translation_opts.localized_fieldnames.items():
|
||||
for ln in loc_names:
|
||||
rev_dict[ln] = orig_name
|
||||
translation_opts.localized_fieldnames_rev = rev_dict
|
||||
# Add translation fields to the model.
|
||||
add_translation_fields(model, opts)
|
||||
|
||||
# Delete all fields cache for related model (parent and children)
|
||||
for related_obj in model._meta.get_all_related_objects():
|
||||
|
|
@ -210,9 +233,9 @@ class Translator(object):
|
|||
patch_constructor(model)
|
||||
|
||||
# Substitute original field with descriptor
|
||||
model_fallback_values = getattr(translation_opts, 'fallback_values', None)
|
||||
model_fallback_languages = getattr(translation_opts, 'fallback_languages', None)
|
||||
for field_name in translation_opts.fields:
|
||||
model_fallback_values = getattr(opts, 'fallback_values', None)
|
||||
model_fallback_languages = getattr(opts, 'fallback_languages', None)
|
||||
for field_name in opts.local_fields.iterkeys():
|
||||
if model_fallback_values is None:
|
||||
field_fallback_value = None
|
||||
elif isinstance(model_fallback_values, dict):
|
||||
|
|
@ -232,49 +255,72 @@ class Translator(object):
|
|||
"""
|
||||
Unregisters the given model(s).
|
||||
|
||||
If a model isn't already registered, this will raise NotRegistered.
|
||||
If a model isn't registered, this will raise NotRegistered. If one of
|
||||
its subclasses is registered, DescendantRegistered will be raised.
|
||||
"""
|
||||
if isinstance(model_or_iterable, ModelBase):
|
||||
model_or_iterable = [model_or_iterable]
|
||||
for model in model_or_iterable:
|
||||
if model not in self._registry:
|
||||
raise NotRegistered(
|
||||
'The model "%s" is not registered for translation' % model.__name__)
|
||||
del self._registry[model]
|
||||
# Check if the model is actually registered (``get_options_for_model``
|
||||
# throws an exception if it's not).
|
||||
self.get_options_for_model(model)
|
||||
# Invalidate all submodels options and forget about
|
||||
# the model itself.
|
||||
for desc, desc_opts in self._registry.items():
|
||||
if not issubclass(desc, model):
|
||||
continue
|
||||
if model != desc and desc_opts.registered:
|
||||
# Allowing to unregister a base would necessitate
|
||||
# repatching all submodels.
|
||||
raise DescendantRegistered(
|
||||
'You need to unregister descendant "%s" before'
|
||||
' unregistering its base "%s"' %
|
||||
(desc.__name__, model.__name__))
|
||||
del self._registry[desc]
|
||||
|
||||
def get_registered_models(self, abstract=True):
|
||||
"""
|
||||
Returns a list of all registered models, or just concrete
|
||||
registered models.
|
||||
"""
|
||||
return [model for (model, opts) in self._registry.items()
|
||||
if opts.registered and (not model._meta.abstract or abstract)]
|
||||
|
||||
def _get_options_for_model(self, model, opts_class=None, **options):
|
||||
"""
|
||||
Returns an instance of translation options with translated fields
|
||||
defined for the ``model`` and inherited from superclasses.
|
||||
"""
|
||||
if model not in self._registry:
|
||||
# Create a new type for backwards compatibility.
|
||||
opts = type("%sTranslationOptions" % model.__name__,
|
||||
(opts_class or TranslationOptions,), options)(model)
|
||||
|
||||
# Fields for translation may be inherited from abstract
|
||||
# superclasses, so we need to look at all parents.
|
||||
for base in model.__bases__:
|
||||
if not hasattr(base, '_meta'):
|
||||
# Things without _meta aren't functional models, so they're
|
||||
# uninteresting parents.
|
||||
continue
|
||||
opts.update(self._get_options_for_model(base))
|
||||
|
||||
# Cache options for all models -- we may want to compute options
|
||||
# of registered subclasses of unregistered models.
|
||||
self._registry[model] = opts
|
||||
|
||||
return self._registry[model]
|
||||
|
||||
def get_options_for_model(self, model):
|
||||
"""
|
||||
Returns the translation options for the given ``model``. If the
|
||||
``model`` is not registered a ``NotRegistered`` exception is raised.
|
||||
Thin wrapper around ``_get_options_for_model`` to preserve the
|
||||
semantic of throwing exception for models not directly registered.
|
||||
"""
|
||||
try:
|
||||
return self._registry[model]
|
||||
except KeyError:
|
||||
# Try to find a localized parent model and build a dedicated
|
||||
# translation options class with the parent info.
|
||||
# Useful when a ModelB inherits from ModelA and only ModelA fields
|
||||
# are localized. No need to register ModelB.
|
||||
fields = set()
|
||||
localized_fieldnames = {}
|
||||
localized_fieldnames_rev = {}
|
||||
for parent in model._meta.parents.keys():
|
||||
if parent in self._registry:
|
||||
trans_opts = self._registry[parent]
|
||||
fields.update(trans_opts.fields)
|
||||
localized_fieldnames.update(trans_opts.localized_fieldnames)
|
||||
localized_fieldnames_rev.update(trans_opts.localized_fieldnames_rev)
|
||||
if fields and localized_fieldnames and localized_fieldnames_rev:
|
||||
options = {
|
||||
'__module__': __name__,
|
||||
'fields': tuple(fields),
|
||||
'localized_fieldnames': localized_fieldnames,
|
||||
'localized_fieldnames_rev': localized_fieldnames_rev
|
||||
}
|
||||
translation_opts = type(
|
||||
"%sTranslation" % model.__name__, (TranslationOptions,), options)
|
||||
# delete_cache_fields(model)
|
||||
return translation_opts
|
||||
raise NotRegistered('The model "%s" is not registered for translation' % model.__name__)
|
||||
opts = self._get_options_for_model(model)
|
||||
if not opts.registered:
|
||||
raise NotRegistered('The model "%s" is not registered for '
|
||||
'translation' % model.__name__)
|
||||
return opts
|
||||
|
||||
|
||||
# This global object represents the singleton translator object
|
||||
|
|
|
|||
Loading…
Reference in a new issue