Use app-level translation files in favour of a single project-level one. Adds an autoregister feature similiar to the one provided by Django's admin. A new setting MODELTRANSLATION_TRANSLATION_FILES keeps backwards compatibility with older versions. This is basically a merge from django-modeltranslation-wrapper with a few changes regarding how registration is triggered. See documentation for details. Resolves issues 58 and 71 (thanks to Jacek Tomaszewski, the author of modeltranslation-wrapper).

This commit is contained in:
Dirk Eschler 2012-07-10 12:58:08 +00:00
parent e85f776aa8
commit 4579d831e5
6 changed files with 139 additions and 76 deletions

View file

@ -1,6 +1,14 @@
v0.4.0-alpha1
=============
CHANGED: Use app-level translation files in favour of a single project-level
one. Adds an autoregister feature similiar to the one provided by
Django's admin. A new setting MODELTRANSLATION_TRANSLATION_FILES keeps
backwards compatibility with older versions. This is basically a merge
from django-modeltranslation-wrapper with a few changes regarding how
registration is triggered. See documentation for details.
(thanks to Jacek Tomaszewski, the author of modeltranslation-wrapper,
resolves issues 58 and 71)
CHANGED: Moved tests to separate folder and added tests for TranslationAdmin.
To run the tests the settings provided in model.tests.modeltranslation
have to be used (settings.LANGUAGES override doesn't work for

View file

@ -41,15 +41,17 @@ in detail in the following sections:
1. Add the ``modeltranslation`` app to the ``INSTALLED_APPS`` variable of your
project's ``settings.py``.
2. Configure your languages in the ``settings.py``.
2. Configure your ``LANGUAGES`` in ``settings.py``.
3. Create a ``translation.py`` in your project directory and register
3. Create a ``translation.py`` in your app directory and register
``TranslationOptions`` for every model you want to translate.
4. Configure the ``MODELTRANSLATION_TRANSLATION_REGISTRY`` variable in your
4. Configure the ``MODELTRANSLATION_TRANSLATION_FILES`` variable in your
``settings.py``.
5. Sync the database using ``manage.py syncdb`` (note that this only applies
5. Add ``translator.autoregister()`` to your project's ``urls.py``.
6. Sync the database using ``manage.py syncdb`` (note that this only applies
if the models registered in the ``translations.py`` did not have been
synced to the database before. If they did - read further down what to do
in that case.
@ -107,17 +109,26 @@ To override the default language as described in settings.LANGUAGES, define
``MODELTRANSLATION_DEFAULT_LANGUAGE``. Note that the value has to be in
settings.LANGUAGES, otherwise an exception will be raised.
**settings.MODELTRANSLATION_TRANSLATION_REGISTRY**
**settings.MODELTRANSLATION_TRANSLATION_FILES**
In order to be able to import the project's ``translation.py`` registration
file the ``MODELTRANSLATION_TRANSLATION_REGISTRY`` must be set to a value in
the form ``<PROJECT_MODULE>.translation``. E.g. if your project is located in a
folder named ``myproject`` the ``MODELTRANSLATION_TRANSLATION_REGISTRY`` must
be set like this:
*New in 0.4*
In order to be able to import the ``translation.py`` registration files of your
apps, ``MODELTRANSLATION_TRANSLATION_FILES`` must be set to a value in the
form:
::
MODELTRANSLATION_TRANSLATION_REGISTRY = "myproject.translation"
('<APP1_MODULE>.translation',
'<APP2_MODULE>.translation',)
.. note:: Modeltranslation up to version 0.3 used a single project wide
registration file which was defined through
``MODELTRANSLATION_TRANSLATION_REGISTRY = '<PROJECT_MODULE>.translation'``.
For backwards compatibiliy the module defined through this setting is
automatically added to ``MODELTRANSLATION_TRANSLATION_FILES``. A
DeprecationWarning is issued in this case.
**settings.MODELTRANSLATION_CUSTOM_FIELDS**
@ -152,13 +163,13 @@ option class containg the fields to translate is registered with the
Registering models and their fields for translation requires the following
steps:
1. Create a ``translation.py`` in your project directory.
1. Create a ``translation.py`` in your app directory.
2. Create a translation option class for every model to translate.
3. Register the model and the translation option class at the
``modeltranslation.translator.translator``
The ``modeltranslation`` application reads the ``translation.py`` file in your
project directory thereby triggering the registration of the translation
app directory thereby triggering the registration of the translation
options found in the file.
A translation option is a class that declares which fields of a model to
@ -167,10 +178,6 @@ and it must provide a ``fields`` attribute storing the list of fieldnames. The
option class must be registered with the
``modeltranslation.translator.translator`` instance.
.. note:: In contrast to the Django admin application which looks for
``admin.py`` files in the project **and** application directories,
the modeltranslation app looks only for one ``translation.py`` file in the project directory.
To illustrate this let's have a look at a simple example using a ``News``
model. The news in this example only contains a ``title`` and a ``text`` field.
Instead of a news, this could be any Django model class:
@ -205,6 +212,27 @@ translation will have been added some auto-magical fields. The next section
explains how things are working under the hood.
Autoregister translation files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Similiar to Django's admin, modeltranslation provides a mechanism to
automatically register your translation files. It has to be hooked into your
project's urls.py.
::
from modeltranslation import translator
translator.autodiscover()
# ...
# Must be in in front of:
from django.contrib import admin
admin.autodiscover()
.. note:: This step is basically optional if you prefer to register your models
programmatically.
Changes automatically applied to the model class
------------------------------------------------
After registering the ``News`` model for transaltion an SQL dump of the
@ -359,21 +387,54 @@ patching on all your models registered for translation:
Tweaks applied to the admin
---------------------------
The ``TranslationAdmin`` class does only implement one special method which is
``def formfield_for_dbfield(self, db_field, **kwargs)``. This method does the
following:
1. Removes the original field from every admin form by setting it
``editable=False``.
2. Copies the widget of the original field to each of it's translation fields.
3. Checks if the - now removed - original field was required and if so makes
formfield_for_dbfield
~~~~~~~~~~~~~~~~~~~~~
The ``TranslationBaseModelAdmin`` class, which ``TranslationAdmin`` and all
inline related classes in modeltranslation derive from, implements a special
method which is ``def formfield_for_dbfield(self, db_field, **kwargs)``. This
method does the following:
1. Copies the widget of the original field to each of it's translation fields.
2. Checks if the original field was required and if so makes
the default translation field required instead.
get_form and get_fieldsets
~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``TranslationBaseModelAdmin`` class overrides ``get_form``,
``get_fieldsets`` and ``_declared_fieldsets`` to make the options ``fields``,
``exclude`` and ``fieldsets`` work in a transparent way. It basically does:
1. Removes the original field from every admin form by adding it to
``exclude`` under the hood.
2. Replaces the - now removed - orginal fields with their corresponding
translation fields.
Taken the ``fieldsets`` option as an example, where the ``title`` field is
registered for translation but not the ``news`` field:
::
class NewsAdmin(TranslationAdmin):
fieldsets = [
(u'News', {'fields': ('title', 'news',)})
]
In this case ``get_fieldsets`` will return a patched fieldset which contains
the translation fields of ``title``, but not the original field:
::
>>> a = NewsAdmin(NewsModel, site)
>>> a.get_fieldsets(request)
[(u'News', {'fields': ('title_de', 'title_en', 'news',)})]
.. _translationadmin_in_combination_with_other_admin_classes:
!TranslationAdmin in combination with other admin classes
---------------------------------------------------------
TranslationAdmin in combination with other admin classes
--------------------------------------------------------
If there already exists a custom admin class for a translated model and you
don't want or can't edit that class directly there is another solution.
@ -387,7 +448,8 @@ inheritance like this:
class MyTranslatedNewsAdmin(NewsAdmin, TranslationAdmin):
pass
In a more complex setup the NewsAdmin might define its own class:
In a more complex setup the NewsAdmin itself might override
formfield_for_dbfield:
::

View file

@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
import sys
from django.conf import settings
from django.db import models
from modeltranslation.settings import TRANSLATION_REGISTRY
from modeltranslation.translator import translator
# Every model registered with the modeltranslation.translator.translator is
# patched to contain additional localized versions for every field specified
# in the model's translation options.
# Import the project's global "translation.py" which registers model classes
# and their translation options with the translator object.
try:
__import__(TRANSLATION_REGISTRY, {}, {}, [''])
except ImportError:
sys.stderr.write("modeltranslation: Can't import module '%s'.\n"
"(If the module exists, it's causing an ImportError "
"somehow.)\n" % TRANSLATION_REGISTRY)
# For some reason ImportErrors raised in translation.py or in modules that
# are included from there become swallowed. Work around this problem by
# printing the traceback explicitly.
import traceback
traceback.print_exc()
# After importing all translation modules, all translation classes are
# registered with the translator.
if settings.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).' % (len(translator._registry),
translated_model_names))
except IndexError:
pass

View file

@ -6,17 +6,17 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
if hasattr(settings, 'MODELTRANSLATION_TRANSLATION_REGISTRY'):
TRANSLATION_REGISTRY = getattr(
settings, 'MODELTRANSLATION_TRANSLATION_REGISTRY', None)
elif hasattr(settings, 'TRANSLATION_REGISTRY'):
warn('The setting TRANSLATION_REGISTRY is deprecated, use '
'MODELTRANSLATION_TRANSLATION_REGISTRY instead.', DeprecationWarning)
TRANSLATION_REGISTRY = getattr(settings, 'TRANSLATION_REGISTRY', None)
else:
TRANSLATION_FILES = tuple(
getattr(settings, 'MODELTRANSLATION_TRANSLATION_FILES', ()))
TRANSLATION_REGISTRY = getattr(
settings, 'MODELTRANSLATION_TRANSLATION_REGISTRY', None)
if TRANSLATION_REGISTRY:
TRANSLATION_FILES += (TRANSLATION_REGISTRY,)
warn('The setting MODELTRANSLATION_TRANSLATION_REGISTRY is deprecated, '
'use MODELTRANSLATION_TRANSLATION_FILES instead.', DeprecationWarning)
if not TRANSLATION_FILES:
raise ImproperlyConfigured(
"You haven't set the MODELTRANSLATION_TRANSLATION_REGISTRY "
"setting yet.")
"You haven't set the MODELTRANSLATION_TRANSLATION_FILES setting yet.")
AVAILABLE_LANGUAGES = [l[0] for l in settings.LANGUAGES]
DEFAULT_LANGUAGE = getattr(settings, 'MODELTRANSLATION_DEFAULT_LANGUAGE', None)

View file

@ -2,6 +2,8 @@
"""
Tests have to be run with modeltranslation.tests.settings:
./manage.py test --settings=modeltranslation.tests.settings modeltranslation
TODO: Merge autoregister tests from django-modeltranslation-wrapper.
"""
from django import forms
from django.conf import settings

View file

@ -232,5 +232,35 @@ class Translator(object):
'translation' % model.__name__)
def autodiscover():
"""
Auto-discover INSTALLED_APPS translation.py modules and fail silently when
not present. This forces an import on them to register.
Also import explicit modules.
"""
from django.conf import settings
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
from modeltranslation.settings import TRANSLATION_FILES
project_translations = TRANSLATION_FILES
for app in settings.INSTALLED_APPS:
mod = import_module(app)
# Attempt to import the app's translation module.
module = '%s.translation' % app
try:
import_module(module)
except:
# Decide whether to bubble up this error. If the app just
# doesn't have an translation module, we can ignore the error
# attempting to import it, otherwise we want it to bubble up.
if module_has_submodule(mod, 'translation'):
raise
for module in project_translations:
import_module(module)
# This global object represents the singleton translator object
translator = Translator()