Merge pull request #137 from wrwrwr/feature/translation-fields-inheritance

Translation fields inheritance
This commit is contained in:
Dirk Eschler 2013-02-11 02:34:57 -08:00
commit a78639ad63
9 changed files with 330 additions and 183 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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