Skip to content

Commit

Permalink
Finetune documentation and README
Browse files Browse the repository at this point in the history
  • Loading branch information
vdboor committed Jul 7, 2014
1 parent 400c99c commit 8fa38ba
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 163 deletions.
178 changes: 22 additions & 156 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Features:
* Easy to combine with custom Manager or QuerySet classes.
* Easy to construct the translations model manually when needed.

See the documentation_ for more details.

Installation
============
Expand Down Expand Up @@ -143,30 +144,16 @@ Translated fields can be accessed directly::
>>> object.title = "omelette du fromage" # Translation is created on demand.
>>> object.save()

When an attribute is not translated yet, the default language will be returned.
The default language is configured by the ``PARLER_DEFAULT_LANGUAGE_CODE``
or ``PARLER_DEFAULT_LANGUAGE_CODE['default']['fallback']`` setting.

The objects can be fetched in a specific language::

>>> objects = MyModel.objects.language('fr').all()
>>> objects[0].title
u'omelette du fromage'

When an attribute is not translated yet, the default language
(set by ``PARLER_DEFAULT_LANGUAGE_CODE`` or ``PARLER_DEFAULT_LANGUAGE_CODE['default']['fallback']``)
will be retured.


Querying translated attributes
------------------------------

Currently, this package doesn't improve the QuerySet API to access translated fields.
Hence, simply access the translated fields like any normal relation::

object = MyObject.objects.filter(translations__title='omelette')

translation1 = myobject.translations.all()[0]

Note that due to the Django ORM design, the query for translated attributes should
typically occur within a single ``.filter(..)`` call. When using ``.filter(..).filter(..)``,
the ORM turns that into 2 separate joins on the translations table.
See `the ORM documentation <https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships>`_ for more details.


Filtering translated objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -179,11 +166,6 @@ To restrict the queryset to translated objects only, the following methods are a
The ``active_translations()`` method also returns objects which are translated in the fallback language,
unless ``hide_untranslated = True`` is used in the ``PARLER_LANGUAGES`` setting.

.. note::
These methods perform a query on the ``translations__language_code`` field.
Hence, they can't be combined with other filters on translated fields,
as that causes double joins on the translations table.

If you have to query a language and translated attribute, query both in a single ``.filter()`` call::

from parler.utils import get_active_language_choices
Expand All @@ -199,85 +181,26 @@ For convenience, use the provided methods::

MyObject.objects.active_translations(slug='omelette')


Advanced example
----------------

The translated model can be constructed manually too::

from django.db import models
from parler.models import TranslatableModel, TranslatedFieldsModel
from parler.fields import TranslatedField


class MyModel(TranslatableModel):
title = TranslatedField() # Optional, explicitly mention the field

class Meta:
verbose_name = _("MyModel")

def __unicode__(self):
return self.title


class MyModelTranslation(TranslatedFieldsModel):
master = models.ForeignKey(MyModel, related_name='translations', null=True)
title = models.CharField(_("Title"), max_length=200)

class Meta:
verbose_name = _("MyModel translation")


Missing translation fallbacks
-----------------------------

When a translation is missing, the fallback language is used.
However, when an object only exists in a different language, this still fails.

This package provides 3 solutions to this problem:

1. Declare the translated attribute explicitly with ``any_language=True``::

class MyModel(TranslatableModel):
title = TranslatedField(any_language=True)

Now, the title will try to fetch one of the existing languages from the database.

2. Use ``model.safe_translation_getter("fieldname", any_language=True)`` on attributes
which don't have an ``any_language=True`` setting.

3. Use a ``try .. catch TranslationDoesNotExist`` block for custom handling.
Because this exception inherits from ``AttibuteError``, templates typically display empty values by default.

4. Avoid fetching those objects using something like: ``queryset.active_translations()`` or ``queryset.filter(translations__language_code__in=('nl', 'en')).distinct()``.
Note that the same `ORM restrictions <https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships>`_ apply here.
.. note::
Due to Django ORM design, queries on the translated fields model should occur in a single ``.filter(..)`` or ``.translated(..)`` call.
When using ``.filter(..).filter(..)``, the ORM turns that into 2 separate joins on the translations table.
See `the ORM documentation <https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships>`_ for more details.


Background story
================
Advanced Features
-----------------

This package is inspired by django-hvad_. When attempting to integrate multilingual
support into django-fluent-pages_ using django-hvad_ this turned out to be really hard.
The sad truth is that while django-hvad_ has a nice admin interface, table layout and model API,
it also overrides much of the default behavior of querysets and model metaclasses.
Currently, this prevents combining django-hvad_ with django-polymorphic_.
This package also includes:

When investigating other multilingual packages, they either appeared to be outdated,
store translations in the same table (too inflexible for us) or only provided a model API.
Hence, there was a need for a new solution, using a simple, crude but effective API.
* Creating the ``TranslatedFieldsModel`` manually!
* Form classes for inline support.
* View classes for switching languages, creating/updating translatable objects.
* Template tags for language switching-buttons.
* ORM methods to handle the translated fields.
* Admin inlines support.

Initially, multilingual support was coded directly within django-fluent-pages_,
while keeping a future django-hvad_ transition in mind. Instead of doing metaclass operations,
the "shared model" just proxied all attributes to the translated model (all manually constructed).
Queries just had to be performed using ``.filter(translations__title=..)``.
This proved to be a sane solution and quickly it turned out that this code
deserved a separate package, and some other modules needed it too.
See the documentation_ for more details.

This package is an attempt to combine the best of both worlds;
the API simplicity of django-hvad_ with the crude,
but effective solution of proxying translated attributes.
And yes, we've added some metaclass magic too - to make life easier -
without loosing the freedom of manually using the API at your will.

TODO
====
Expand All @@ -290,64 +213,6 @@ TODO
Please contribute your improvements or work on these area's!


Django compatibility
====================

This package has been tested with Django 1.4 and 1.5 on Python 2.6/2.7.

Django 1.4 note
---------------

When using Django 1.4, there is a small tweak you'll have to make in the admin.
Instead of using ``fieldsets = ..``, use ``declared_fieldsets = ..``
on the ``ModelAdmin`` definition. The Django 1.4 admin validation doesn't actualy
check the form fields, but only checks whether the fields exist in the model - which they obviously don't.
Using ``declared_fieldsets`` instead of ``fieldsets`` circumvents this check.


API
====

On ``parler.models.TranslatableModel``:

* ``get_current_language()``
* ``set_current_language(language_code, initialize=False)``
* ``get_fallback_language()``
* ``get_available_languages()``
* ``has_translation(language_code=None)``
* ``save_translations()``
* ``safe_translation_getter(field, default=None, any_language=False)``

On ``parler.models.TranslatedFieldsModel``:

* ``language_code`` - The language code field.
* ``master`` - ForeignKey to the shared table.
* ``is_modified`` - Property to detect changes.
* ``get_translated_fields()`` - The names of translated fields.

On ``parler.managers.TranslatableManager``:

* ``queryset_class`` - the attribute that points to the queryset class.
* ``language(language_code=None)`` - set the language of returned objects.
* ``translated(*language_codes)`` - return only translated objects (NOTE: can't be combined with other filters)
* ``active_translations(language_code=None)`` - return objects of the currently active translation (may include the fallback language too).

On ``parler.admin.TranslatableAdmin``:

* ``get_form_language(request, obj=None)`` - return the currently active language in the admin form.
* ``get_available_languages(obj)`` - returns the QuerySet with all active languages.
* ``language_column(obj)`` - the extra column which can be added to the ``list_display``.

In ``parler.utils``:

* ``normalize_language_code()``
* ``is_supported_django_language()``
* ``get_language_title()``
* ``get_language_settings()``
* ``get_active_language_choices()``
* ``is_multilingual_project()``


Contributing
============

Expand All @@ -363,3 +228,4 @@ Pull requests are welcome too. :-)
.. _django-mptt: https://github.com/django-mptt/django-mptt
.. _django-fluent-pages: https://github.com/edoburu/django-fluent-pages
.. _django-polymorphic: https://github.com/chrisglass/django_polymorphic
.. _documentation: http://django-parler.readthedocs.org/
18 changes: 18 additions & 0 deletions docs/compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,21 @@ on the :class:`~django.contrib.admin.ModelAdmin` definition.
The Django 1.4 admin validation doesn't actually check the form fields,
but only checks whether the fields exist in the model - which they obviously don't.
Using ``declared_fieldsets`` instead of ``fieldsets`` circumvents this check.

Using prepopulated_fields
-------------------------

Using :attr:`~django.contrib.admin.ModelAdmin.prepopulated_fields` doesn't work yet,
as the admin will complain that the field does not exist.
Use :func:`~django.contrib.admin.ModelAdmin.get_prepopulated_fields` as workaround::

from parler.admin import TranslatableAdmin

class MyModelAdmin(TranslatableAdmin):

def get_prepopulated_fields(self, request, obj=None):
# can't use `prepopulated_fields = ..` because it breaks the admin validation
# for translated fields. This is the official django-parler workaround.
return {
'slug': ('title',)
}
17 changes: 10 additions & 7 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,20 @@ The translated fields can be accessed directly::
>>> object.title = "omelette du fromage" # Translation is created on demand.
>>> object.save()

When fetching objects, the :func:`~parler.managers.TranslatableManager.language` method
can be used to configure the active language of the returned objects.
By default, the current Django language is used.
When an attribute is not translated yet, the default language will be returned.
The default language is configured by the :ref:`PARLER_DEFAULT_LANGUAGE_CODE`
or ``PARLER_DEFAULT_LANGUAGE_CODE['default']['fallback']`` setting.

The objects can be fetched in a specific language using
the :func:`~parler.managers.TranslatableManager.language` method.

>>> objects = MyModel.objects.language('fr').all()
>>> objects[0].title
u'omelette du fromage'

When an attribute is not translated yet, the default language
(set by :ref:`PARLER_DEFAULT_LANGUAGE_CODE` or ``PARLER_DEFAULT_LANGUAGE_CODE['default']['fallback']``)
will be returned.
This only sets the language of the object.
By default, the current Django language is used.
To filter the objects, use the :func:`~parler.managers.TranslatableManager.translated` method.


Querying translated attributes
Expand All @@ -130,7 +133,7 @@ Querying translated attributes
To restrict the queryset to translated objects only, the following methods are available:

* :func:`MyObject.objects.translated(*language_codes, **translated_fields) <parler.managers.TranslatableManager.translated>` - return only objects with a translation of ``language_codes``.
* :func:`MyObject.objects.active_translations(language_code=None, **translated_fields) <parler.managers.TranslatableManager.active_translations` - return only objects for the current language (and fallback if this applies).
* :func:`MyObject.objects.active_translations(language_code=None, **translated_fields) <parler.managers.TranslatableManager.active_translations>` - return only objects for the current language (and fallback if this applies).

The :func:`parler.managers.TranslatableManager.active_translations` method also returns objects which are translated in the fallback language,
unless ``hide_untranslated = True`` is used in the :ref:`PARLER_LANGUAGES`` setting.
Expand Down

0 comments on commit 8fa38ba

Please sign in to comment.