From 4579d831e564ce70146f1d5ec0576198e9ac7e47 Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Tue, 10 Jul 2012 12:58:08 +0000 Subject: [PATCH] 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). --- CHANGELOG.txt | 8 ++ docs/modeltranslation/modeltranslation.txt | 116 ++++++++++++++++----- modeltranslation/models.py | 39 ------- modeltranslation/settings.py | 20 ++-- modeltranslation/tests/__init__.py | 2 + modeltranslation/translator.py | 30 ++++++ 6 files changed, 139 insertions(+), 76 deletions(-) delete mode 100644 modeltranslation/models.py diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1cd290f..2c41550 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -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 diff --git a/docs/modeltranslation/modeltranslation.txt b/docs/modeltranslation/modeltranslation.txt index b124ff0..8e0285d 100644 --- a/docs/modeltranslation/modeltranslation.txt +++ b/docs/modeltranslation/modeltranslation.txt @@ -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 ``.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" + ('.translation', + '.translation',) + +.. note:: Modeltranslation up to version 0.3 used a single project wide + registration file which was defined through + ``MODELTRANSLATION_TRANSLATION_REGISTRY = '.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: :: diff --git a/modeltranslation/models.py b/modeltranslation/models.py deleted file mode 100644 index e64c964..0000000 --- a/modeltranslation/models.py +++ /dev/null @@ -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 diff --git a/modeltranslation/settings.py b/modeltranslation/settings.py index e534308..73e7c4f 100644 --- a/modeltranslation/settings.py +++ b/modeltranslation/settings.py @@ -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) diff --git a/modeltranslation/tests/__init__.py b/modeltranslation/tests/__init__.py index 41b1d15..fd7e2be 100644 --- a/modeltranslation/tests/__init__.py +++ b/modeltranslation/tests/__init__.py @@ -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 diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index ca07008..9f0d165 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -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()