Completed
Push — master ( 0bffe1...d6203d )
by Diederik van der
11s
created

TranslatableModelMixin.get_available_languages()   B

Complexity

Conditions 6

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 21
rs 7.8867
cc 6
1
"""
2
The models and fields for translation support.
3
4
The default is to use the :class:`TranslatedFields` class in the model, like:
5
6
.. code-block:: python
7
8
    from django.db import models
9
    from django.utils.encoding import python_2_unicode_compatible
10
    from parler.models import TranslatableModel, TranslatedFields
11
12
13
    @python_2_unicode_compatible
14
    class MyModel(TranslatableModel):
15
        translations = TranslatedFields(
16
            title = models.CharField(_("Title"), max_length=200)
17
        )
18
19
        class Meta:
20
            verbose_name = _("MyModel")
21
22
        def __str__(self):
23
            return self.title
24
25
26
It's also possible to create the translated fields model manually:
27
28
.. code-block:: python
29
30
    from django.db import models
31
    from django.utils.encoding import python_2_unicode_compatible
32
    from parler.models import TranslatableModel, TranslatedFieldsModel
33
    from parler.fields import TranslatedField
34
35
36
    class MyModel(TranslatableModel):
37
        title = TranslatedField()  # Optional, explicitly mention the field
38
39
        class Meta:
40
            verbose_name = _("MyModel")
41
42
        def __str__(self):
43
            return self.title
44
45
46
    class MyModelTranslation(TranslatedFieldsModel):
47
        master = models.ForeignKey(MyModel, related_name='translations', null=True)
48
        title = models.CharField(_("Title"), max_length=200)
49
50
        class Meta:
51
            verbose_name = _("MyModel translation")
52
53
This has the same effect, but also allows to to override
54
the :func:`~django.db.models.Model.save` method, or add new methods yourself.
55
56
The translated model is compatible with django-hvad, making the transition between both projects relatively easy.
57
The manager and queryset objects of django-parler can work together with django-mptt and django-polymorphic.
58
"""
59
from __future__ import unicode_literals
60
from collections import defaultdict
61
import django
62
from django.conf import settings
63
from django.core.exceptions import ImproperlyConfigured, ValidationError, FieldError, ObjectDoesNotExist
64
from django.db import models, router
65
from django.db.models.base import ModelBase
66
from django.utils.encoding import python_2_unicode_compatible
67
from django.utils.functional import lazy
68
from django.utils.translation import get_language, ugettext, ugettext_lazy as _
69
from django.utils import six
70
from parler import signals
71
from parler.cache import MISSING, _cache_translation, _cache_translation_needs_fallback, _delete_cached_translation, get_cached_translation, _delete_cached_translations, get_cached_translated_field
72
from parler.fields import TranslatedField, LanguageCodeDescriptor, TranslatedFieldDescriptor
73
from parler.managers import TranslatableManager
74
from parler.utils import compat
75
from parler.utils.i18n import normalize_language_code, get_language_settings, get_language_title
76
import sys
77
78
try:
79
    from collections import OrderedDict
80
except ImportError:
81
    from django.utils.datastructures import SortedDict as OrderedDict
82
83
if django.VERSION >= (1, 9):
84
    from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
85
else:
86
    from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor
87
88
__all__ = (
89
    'TranslatableModelMixin',
90
    'TranslatableModel',
91
    'TranslatedFields',
92
    'TranslatedFieldsModel',
93
    'TranslatedFieldsModelBase',
94
    'TranslationDoesNotExist',
95
    #'create_translations_model',
96
)
97
98
99
class TranslationDoesNotExist(AttributeError, ObjectDoesNotExist):
100
    """
101
    A tagging interface to detect missing translations.
102
    The exception inherits from :class:`~exceptions.AttributeError` to reflect what is actually happening.
103
    Therefore it also causes the templates to handle the missing attributes silently, which is very useful in the admin for example.
104
    The exception also inherits from :class:`~django.core.exceptions.ObjectDoesNotExist`,
105
    so any code that checks for this can deal with missing translations out of the box.
106
107
    This class is also used in the ``DoesNotExist`` object on the translated model, which inherits from:
108
109
    * this class
110
    * the ``sharedmodel.DoesNotExist`` class
111
    * the original ``translatedmodel.DoesNotExist`` class.
112
113
    This makes sure that the regular code flow is decently handled by existing exception handlers.
114
    """
115
    pass
116
117
118
_lazy_verbose_name = lazy(lambda x: ugettext("{0} Translation").format(x._meta.verbose_name), six.text_type)
119
120
121
def create_translations_model(shared_model, related_name, meta, **fields):
122
    """
123
    Dynamically create the translations model.
124
    Create the translations model for the shared model 'model'.
125
126
    :param related_name: The related name for the reverse FK from the translations model.
127
    :param meta: A (optional) dictionary of attributes for the translations model's inner Meta class.
128
    :param fields: A dictionary of fields to put on the translations model.
129
130
    Two fields are enforced on the translations model:
131
132
        language_code: A 15 char, db indexed field.
133
        master: A ForeignKey back to the shared model.
134
135
    Those two fields are unique together.
136
    """
137
    if not meta:
138
        meta = {}
139
140
    if shared_model._meta.abstract:
141
        # This can't be done, because `master = ForeignKey(shared_model)` would fail.
142
        raise TypeError("Can't create TranslatedFieldsModel for abstract class {0}".format(shared_model.__name__))
143
144
    # Define inner Meta class
145
    meta['app_label'] = shared_model._meta.app_label
146
    meta['db_tablespace'] = shared_model._meta.db_tablespace
147
    meta['managed'] = shared_model._meta.managed
148
    meta['unique_together'] = list(meta.get('unique_together', [])) + [('language_code', 'master')]
149
    meta.setdefault('db_table', '{0}_translation'.format(shared_model._meta.db_table))
150
    meta.setdefault('verbose_name', _lazy_verbose_name(shared_model))
151
152
    # Avoid creating permissions for the translated model, these are not used at all.
153
    # This also avoids creating lengthy permission names above 50 chars.
154
    if django.VERSION >= (1, 7):
155
        meta.setdefault('default_permissions', ())
156
157
    # Define attributes for translation table
158
    name = str('{0}Translation'.format(shared_model.__name__))  # makes it bytes, for type()
159
160
    attrs = {}
161
    attrs.update(fields)
162
    attrs['Meta'] = type(str('Meta'), (object,), meta)
163
    attrs['__module__'] = shared_model.__module__
164
    attrs['objects'] = models.Manager()
165
    attrs['master'] = models.ForeignKey(shared_model, related_name=related_name, editable=False, null=True)
166
167
    # Create and return the new model
168
    translations_model = TranslatedFieldsModelBase(name, (TranslatedFieldsModel,), attrs)
169
170
    # Register it as a global in the shared model's module.
171
    # This is needed so that Translation model instances, and objects which refer to them, can be properly pickled and unpickled.
172
    # The Django session and caching frameworks, in particular, depend on this behaviour.
173
    mod = sys.modules[shared_model.__module__]
174
    setattr(mod, name, translations_model)
175
176
    return translations_model
177
178
179
class TranslatedFields(object):
180
    """
181
    Wrapper class to define translated fields on a model.
182
183
    The field name becomes the related name of the :class:`TranslatedFieldsModel` subclass.
184
185
    Example:
186
187
    .. code-block:: python
188
189
        from django.db import models
190
        from parler.models import TranslatableModel, TranslatedFields
191
192
        class MyModel(TranslatableModel):
193
            translations = TranslatedFields(
194
                title = models.CharField("Title", max_length=200)
195
            )
196
197
    When the class is initialized, the attribute will point
198
    to a :class:`~django.db.models.fields.related.ForeignRelatedObjectsDescriptor` object.
199
    Hence, accessing ``MyModel.translations.related.model`` returns the original model
200
    via the :class:`django.db.models.related.RelatedObject` class.
201
202
    ..
203
       To fetch the attribute, you can also query the Parler metadata:
204
       MyModel._parler_meta.get_model_by_related_name('translations')
205
    """
206
207
    def __init__(self, meta=None, **fields):
208
        self.fields = fields
209
        self.meta = meta
210
        self.name = None
211
212
    def contribute_to_class(self, cls, name):
213
        # Called from django.db.models.base.ModelBase.__new__
214
        self.name = name
215
        create_translations_model(cls, name, self.meta, **self.fields)
216
217
218
class TranslatableModelMixin(object):
219
    """
220
    Base model mixin class to handle translations.
221
222
    All translatable fields will appear on this model, proxying the calls to the :class:`TranslatedFieldsModel`.
223
    """
224
    #: Access to the metadata of the translatable model
225
    _parler_meta = None
226
227
    #: Access to the language code
228
    language_code = LanguageCodeDescriptor()
229
230
    def __init__(self, *args, **kwargs):
231
        # Still allow to pass the translated fields (e.g. title=...) to this function.
232
        translated_kwargs = {}
233
        current_language = None
234
        if kwargs:
235
            current_language = kwargs.pop('_current_language', None)
236
            for field in self._parler_meta.get_all_fields():
237
                try:
238
                    translated_kwargs[field] = kwargs.pop(field)
239
                except KeyError:
240
                    pass
241
242
        # Have the attributes available, but they can't be ready yet;
243
        # self._state.adding is always True at this point,
244
        # the QuerySet.iterator() code changes it after construction.
245
        self._translations_cache = None
246
        self._current_language = None
247
248
        # Run original Django model __init__
249
        super(TranslatableModelMixin, self).__init__(*args, **kwargs)
250
251
        # Assign translated args manually.
252
        self._translations_cache = defaultdict(dict)
253
        self._current_language = normalize_language_code(current_language or get_language())  # What you used to fetch the object is what you get.
254
255
        if translated_kwargs:
256
            self._set_translated_fields(self._current_language, **translated_kwargs)
257
258
    def _set_translated_fields(self, language_code=None, **fields):
259
        """
260
        Assign fields to the translated models.
261
        """
262
        objects = []  # no generator, make sure objects are all filled first
263
        for parler_meta, model_fields in self._parler_meta._split_fields(**fields):
264
            translation = self._get_translated_model(language_code=language_code, auto_create=True, meta=parler_meta)
265
            for field, value in six.iteritems(model_fields):
266
                setattr(translation, field, value)
267
268
            objects.append(translation)
269
        return objects
270
271
    def create_translation(self, language_code, **fields):
272
        """
273
        Add a translation to the model.
274
275
        The :func:`save_translations` function is called afterwards.
276
277
        The object will be saved immediately, similar to
278
        calling :func:`~django.db.models.manager.Manager.create`
279
        or :func:`~django.db.models.fields.related.RelatedManager.create` on related fields.
280
        """
281
        meta = self._parler_meta
282
        if self._translations_cache[meta.root_model].get(language_code, None):  # MISSING evaluates to False too
283
            raise ValueError("Translation already exists: {0}".format(language_code))
284
285
        # Save all fields in the proper translated model.
286
        for translation in self._set_translated_fields(language_code, **fields):
287
            self.save_translation(translation)
288
289
    def get_current_language(self):
290
        """
291
        Get the current language.
292
        """
293
        # not a property, so won't conflict with model fields.
294
        return self._current_language
295
296
    def set_current_language(self, language_code, initialize=False):
297
        """
298
        Switch the currently activate language of the object.
299
        """
300
        self._current_language = normalize_language_code(language_code or get_language())
301
302
        # Ensure the translation is present for __get__ queries.
303
        if initialize:
304
            self._get_translated_model(use_fallback=False, auto_create=True)
305
306
    def get_fallback_language(self):
307
        """
308
        .. deprecated:: 1.5
309
           Use :func:`get_fallback_languages` instead.
310
        """
311
        fallbacks = self.get_fallback_languages()
312
        return fallbacks[0] if fallbacks else None
313
314
    def get_fallback_languages(self):
315
        """
316
        Return the fallback language codes,
317
        which are used in case there is no translation for the currently active language.
318
        """
319
        lang_dict = get_language_settings(self._current_language)
320
        fallbacks = [lang for lang in lang_dict['fallbacks'] if lang != self._current_language]
321
        return fallbacks or []
322
323
    def has_translation(self, language_code=None, related_name=None):
324
        """
325
        Return whether a translation for the given language exists.
326
        Defaults to the current language code.
327
328
        .. versionadded 1.2 Added the ``related_name`` parameter.
329
        """
330
        if language_code is None:
331
            language_code = self._current_language
332
333
        meta = self._parler_meta._get_extension_by_related_name(related_name)
334
335
        try:
336
            # Check the local cache directly, and the answer is known.
337
            # NOTE this may also return newly auto created translations which are not saved yet.
338
            return self._translations_cache[meta.model][language_code] is not MISSING
339
        except KeyError:
340
            # If there is a prefetch, will be using that.
341
            # However, don't assume the prefetch contains all possible languages.
342
            # With Django 1.8, there are custom Prefetch objects.
343
            # TODO: improve this, detect whether this is the case.
344
            if language_code in self._read_prefetched_translations(meta=meta):
345
                return True
346
347
            # Try to fetch from the cache first.
348
            # If the cache returns the fallback, it means the original does not exist.
349
            object = get_cached_translation(self, language_code, related_name=related_name, use_fallback=True)
350
            if object is not None:
351
                return object.language_code == language_code
352
353
            try:
354
                # Fetch from DB, fill the cache.
355
                self._get_translated_model(language_code, use_fallback=False, auto_create=False, meta=meta)
356
            except meta.model.DoesNotExist:
357
                return False
358
            else:
359
                return True
360
361
    def get_available_languages(self, related_name=None, include_unsaved=False):
362
        """
363
        Return the language codes of all translated variations.
364
365
        .. versionadded 1.2 Added the ``include_unsaved`` and ``related_name`` parameters.
366
        """
367
        meta = self._parler_meta._get_extension_by_related_name(related_name)
368
369
        prefetch = self._get_prefetched_translations(meta=meta)
370
        if prefetch is not None:
371
            # TODO: this will break when using custom Django 1.8 Prefetch objects?
372
            db_languages = sorted(obj.language_code for obj in prefetch)
373
        else:
374
            qs = self._get_translated_queryset(meta=meta)
375
            db_languages = qs.values_list('language_code', flat=True).order_by('language_code')
376
377
        if include_unsaved:
378
            local_languages = (k for k, v in six.iteritems(self._translations_cache[meta.model]) if v is not MISSING)
379
            return list(set(db_languages) | set(local_languages))
380
        else:
381
            return db_languages
382
383
    def get_translation(self, language_code, related_name=None):
384
        """
385
        Fetch the translated model
386
        """
387
        meta = self._parler_meta._get_extension_by_related_name(related_name)
388
        return self._get_translated_model(language_code, meta=meta)
389
390
    def _get_translated_model(self, language_code=None, use_fallback=False, auto_create=False, meta=None):
391
        """
392
        Fetch the translated fields model.
393
        """
394
        if self._parler_meta is None:
395
            raise ImproperlyConfigured("No translation is assigned to the current model!")
396
        if self._translations_cache is None:
397
            raise RuntimeError("Accessing translated fields before super.__init__() is not possible.")
398
399
        if not language_code:
400
            language_code = self._current_language
401
        if meta is None:
402
            meta = self._parler_meta.root  # work on base model by default
403
404
        local_cache = self._translations_cache[meta.model]
405
406
        # 1. fetch the object from the local cache
407
        try:
408
            object = local_cache[language_code]
409
410
            # If cached object indicates the language doesn't exist, need to query the fallback.
411
            if object is not MISSING:
412
                return object
413
        except KeyError:
414
            # 2. No cache, need to query
415
            # Check that this object already exists, would be pointless otherwise to check for a translation.
416
            if not self._state.adding and self.pk is not None:
417
                prefetch = self._get_prefetched_translations(meta=meta)
418
                if prefetch is not None:
419
                    # 2.1, use prefetched data
420
                    # If the object is not found in the prefetched data (which contains all translations),
421
                    # it's pointless to check for memcached (2.2) or perform a single query (2.3)
422
                    for object in prefetch:
423
                        if object.language_code == language_code:
424
                            local_cache[language_code] = object
425
                            _cache_translation(object)  # Store in memcached
426
                            return object
427
                else:
428
                    # 2.2, fetch from memcached
429
                    object = get_cached_translation(self, language_code, related_name=meta.rel_name, use_fallback=use_fallback)
430
                    if object is not None:
431
                        # Track in local cache
432
                        if object.language_code != language_code:
433
                            local_cache[language_code] = MISSING  # Set fallback marker
434
                        local_cache[object.language_code] = object
435
                        return object
436
                    elif local_cache.get(language_code, None) is MISSING:
437
                        # If get_cached_translation() explicitly set the "does not exist" marker,
438
                        # there is no need to try a database query.
439
                        pass
440
                    else:
441
                        # 2.3, fetch from database
442
                        try:
443
                            object = self._get_translated_queryset(meta).get(language_code=language_code)
444
                        except meta.model.DoesNotExist:
445
                            pass
446
                        else:
447
                            local_cache[language_code] = object
448
                            _cache_translation(object)  # Store in memcached
449
                            return object
450
451
        # Not in cache, or default.
452
        # Not fetched from DB
453
454
        # 3. Auto create?
455
        if auto_create:
456
            # Auto create policy first (e.g. a __set__ call)
457
            kwargs = {
458
                'language_code': language_code,
459
            }
460
            if self.pk:
461
                # ID might be None at this point, and Django 1.8 does not allow that.
462
                kwargs['master'] = self
463
464
            object = meta.model(**kwargs)
465
            local_cache[language_code] = object
466
            # Not stored in memcached here yet, first fill + save it.
467
            return object
468
469
        # 4. Fallback?
470
        fallback_msg = None
471
        lang_dict = get_language_settings(language_code)
472
473
        if language_code not in local_cache:
474
            # Explicitly set a marker for the fact that this translation uses the fallback instead.
475
            # Avoid making that query again.
476
            local_cache[language_code] = MISSING  # None value is the marker.
477
            if not self._state.adding or self.pk is not None:
478
                _cache_translation_needs_fallback(self, language_code, related_name=meta.rel_name)
479
480
        fallback_choices = [lang_dict['code']] + list(lang_dict['fallbacks'])
481
        if use_fallback and fallback_choices:
482
            # Jump to fallback language, return directly.
483
            # Don't cache under this language_code
484
            for fallback_lang in fallback_choices:
485
                if fallback_lang == language_code:  # Skip the current language, could also be fallback 1 of 2 choices
486
                    continue
487
488
                try:
489
                    return self._get_translated_model(fallback_lang, use_fallback=False, auto_create=auto_create, meta=meta)
490
                except meta.model.DoesNotExist:
491
                    pass
492
493
            fallback_msg = " (tried fallbacks {0})".format(', '.join(lang_dict['fallbacks']))
494
495
        # None of the above, bail out!
496
        raise meta.model.DoesNotExist(
497
            "{0} does not have a translation for the current language!\n"
498
            "{0} ID #{1}, language={2}{3}".format(self._meta.verbose_name, self.pk, language_code, fallback_msg or ''
499
        ))
500
501
    def _get_any_translated_model(self, meta=None):
502
        """
503
        Return any available translation.
504
        Returns None if there are no translations at all.
505
        """
506
        if meta is None:
507
            meta = self._parler_meta.root
508
509
        tr_model = meta.model
510
        local_cache = self._translations_cache[tr_model]
511
        if local_cache:
512
            # There is already a language available in the case. No need for queries.
513
            # Give consistent answers if they exist.
514
            check_languages = [self._current_language] + self.get_fallback_languages()
515
            try:
516
                for fallback_lang in check_languages:
517
                    trans = local_cache.get(fallback_lang, None)
518
                    if trans:
519
                        return trans
520
                return next(t for t in six.itervalues(local_cache) if t is not MISSING)
521
            except StopIteration:
522
                pass
523
524
        try:
525
            # Use prefetch if available, otherwise perform separate query.
526
            prefetch = self._get_prefetched_translations(meta=meta)
527
            if prefetch is not None:
528
                translation = prefetch[0]  # Already a list
529
            else:
530
                translation = self._get_translated_queryset(meta=meta)[0]
531
        except IndexError:
532
            return None
533
        else:
534
            local_cache[translation.language_code] = translation
535
            _cache_translation(translation)
536
            return translation
537
538
    def _get_translated_queryset(self, meta=None):
539
        """
540
        Return the queryset that points to the translated model.
541
        If there is a prefetch, it can be read from this queryset.
542
        """
543
        # Get via self.TRANSLATIONS_FIELD.get(..) so it also uses the prefetch/select_related cache.
544
        if meta is None:
545
            meta = self._parler_meta.root
546
547
        accessor = getattr(self, meta.rel_name)
548
        if django.VERSION >= (1, 6):
549
            # Call latest version
550
            return accessor.get_queryset()
551
        else:
552
            # Must call RelatedManager.get_query_set() and avoid calling a custom get_queryset()
553
            # method for packages with Django 1.6/1.7 compatibility.
554
            return accessor.get_query_set()
555
556
    def _get_prefetched_translations(self, meta=None):
557
        """
558
        Return the queryset with prefetch results.
559
        """
560
        if meta is None:
561
            meta = self._parler_meta.root
562
563
        related_name = meta.rel_name
564
        try:
565
            # Read the list directly, avoid QuerySet construction.
566
            # Accessing self._get_translated_queryset(parler_meta)._prefetch_done is more expensive.
567
            return self._prefetched_objects_cache[related_name]
568
        except (AttributeError, KeyError):
569
            return None
570
571
    def _read_prefetched_translations(self, meta=None):
572
        # Load the prefetched translations into the local cache.
573
        if meta is None:
574
            meta = self._parler_meta.root
575
576
        local_cache = self._translations_cache[meta.model]
577
        prefetch = self._get_prefetched_translations(meta=meta)
578
579
        languages_seen = []
580
        if prefetch is not None:
581
            for translation in prefetch:
582
                lang = translation.language_code
583
                languages_seen.append(lang)
584
                if lang not in local_cache or local_cache[lang] is MISSING:
585
                    local_cache[lang] = translation
586
587
        return languages_seen
588
589
    def save(self, *args, **kwargs):
590
        super(TranslatableModelMixin, self).save(*args, **kwargs)
591
592
        # Makes no sense to add these for translated model
593
        # Even worse: mptt 0.7 injects this parameter when it avoids updating the lft/rgt fields,
594
        # but that misses all the translated fields.
595
        kwargs.pop('update_fields', None)
596
        self.save_translations(*args, **kwargs)
597
598
    def delete(self, using=None):
599
        _delete_cached_translations(self)
600
        super(TranslatableModelMixin, self).delete(using)
601
602
    def validate_unique(self, exclude=None):
603
        """
604
        Also validate the unique_together of the translated model.
605
        """
606
        # This is called from ModelForm._post_clean() or Model.full_clean()
607
        errors = {}
608
        try:
609
            super(TranslatableModelMixin, self).validate_unique(exclude=exclude)
610
        except ValidationError as e:
611
            errors = e.message_dict  # Django 1.5 + 1.6 compatible
612
613
        for local_cache in six.itervalues(self._translations_cache):
614
            for translation in six.itervalues(local_cache):
615
                if translation is MISSING:  # Skip fallback markers
616
                    continue
617
618
                try:
619
                    translation.validate_unique(exclude=exclude)
620
                except ValidationError as e:
621
                    errors.update(e.message_dict)
622
623
        if errors:
624
            raise ValidationError(errors)
625
626
    def save_translations(self, *args, **kwargs):
627
        """
628
        The method to save all translations.
629
        This can be overwritten to implement any custom additions.
630
        This method calls :func:`save_translation` for every fetched language.
631
632
        :param args: Any custom arguments to pass to :func:`save`.
633
        :param kwargs: Any custom arguments to pass to :func:`save`.
634
        """
635
        # Copy cache, new objects (e.g. fallbacks) might be fetched if users override save_translation()
636
        # Not looping over the cache, but using _parler_meta so the translations are processed in the order of inheritance.
637
        local_caches = self._translations_cache.copy()
638
        for meta in self._parler_meta:
639
            local_cache = local_caches[meta.model]
640
            translations = list(local_cache.values())
641
642
            # Save all translated objects which were fetched.
643
            # This also supports switching languages several times, and save everything in the end.
644
            for translation in translations:
645
                if translation is MISSING:  # Skip fallback markers
646
                    continue
647
648
                self.save_translation(translation, *args, **kwargs)
649
650
    def save_translation(self, translation, *args, **kwargs):
651
        """
652
        Save the translation when it's modified, or unsaved.
653
654
        .. note::
655
656
           When a derived model provides additional translated fields,
657
           this method receives both the original and extended translation.
658
           To distinguish between both objects, check for ``translation.related_name``.
659
660
        :param translation: The translation
661
        :type translation: TranslatedFieldsModel
662
        :param args: Any custom arguments to pass to :func:`save`.
663
        :param kwargs: Any custom arguments to pass to :func:`save`.
664
        """
665
        if self.pk is None or self._state.adding:
666
            raise RuntimeError("Can't save translations when the master object is not yet saved.")
667
668
        # Translation models without any fields are also supported.
669
        # This is useful for parent objects that have inlines;
670
        # the parent object defines how many translations there are.
671
        if translation.is_modified or (translation.is_empty and not translation.pk):
672
            if not translation.master_id:  # Might not exist during first construction
673
                translation._state.db = self._state.db
674
                translation.master = self
675
            translation.save(*args, **kwargs)
676
677
    def safe_translation_getter(self, field, default=None, language_code=None, any_language=False):
678
        """
679
        Fetch a translated property, and return a default value
680
        when both the translation and fallback language are missing.
681
682
        When ``any_language=True`` is used, the function also looks
683
        into other languages to find a suitable value. This feature can be useful
684
        for "title" attributes for example, to make sure there is at least something being displayed.
685
        Also consider using ``field = TranslatedField(any_language=True)`` in the model itself,
686
        to make this behavior the default for the given field.
687
688
        .. versionchanged 1.5:: The *default* parameter may also be a callable.
689
        """
690
        meta = self._parler_meta._get_extension_by_field(field)
691
692
        # Extra feature: query a single field from a other translation.
693
        if language_code and language_code != self._current_language:
694
            try:
695
                tr_model = self._get_translated_model(language_code, meta=meta, use_fallback=True)
696
                return getattr(tr_model, field)
697
            except TranslationDoesNotExist:
698
                pass
699
        else:
700
            # By default, query via descriptor (TranslatedFieldDescriptor)
701
            # which also attempts the fallback language if configured to do so.
702
            try:
703
                return getattr(self, field)
704
            except TranslationDoesNotExist:
705
                pass
706
707
        if any_language:
708
            translation = self._get_any_translated_model(meta=meta)
709
            if translation is not None:
710
                try:
711
                    return getattr(translation, field)
712
                except KeyError:
713
                    pass
714
715
        if callable(default):
716
            return default()
717
        else:
718
            return default
719
720
721
class TranslatableModel(TranslatableModelMixin, models.Model):
722
    """
723
    Base model class to handle translations.
724
725
    All translatable fields will appear on this model, proxying the calls to the :class:`TranslatedFieldsModel`.
726
    """
727
    class Meta:
728
        abstract = True
729
730
    # change the default manager to the translation manager
731
    objects = TranslatableManager()
732
733
734
class TranslatedFieldsModelBase(ModelBase):
735
    """
736
    .. versionadded 1.2
737
738
    Meta-class for the translated fields model.
739
740
    It performs the following steps:
741
742
    * It validates the 'master' field, in case it's added manually.
743
    * It tells the original model to use this model for translations.
744
    * It adds the proxy attributes to the shared model.
745
    """
746
    def __new__(mcs, name, bases, attrs):
747
748
        # Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
749
        if not attrs and name == 'NewBase':
750
            if django.VERSION < (1, 5):
751
                # Let Django fully ignore the class which is inserted in between.
752
                # Django 1.5 fixed this, see https://code.djangoproject.com/ticket/19688
753
                attrs['__module__'] = 'django.utils.six'
754
                attrs['Meta'] = type(str('Meta'), (), {'abstract': True})
755
            return super(TranslatedFieldsModelBase, mcs).__new__(mcs, name, bases, attrs)
756
757
        new_class = super(TranslatedFieldsModelBase, mcs).__new__(mcs, name, bases, attrs)
758
        if bases[0] == models.Model:
759
            return new_class
760
761
        # No action in abstract models.
762
        if new_class._meta.abstract or new_class._meta.proxy:
763
            return new_class
764
765
        # Validate a manually configured class.
766
        shared_model = _validate_master(new_class)
767
768
        # Add wrappers for all translated fields to the shared models.
769
        new_class.contribute_translations(shared_model)
770
771
        return new_class
772
773
774
def _validate_master(new_class):
775
    """
776
    Check whether the 'master' field on a TranslatedFieldsModel is correctly configured.
777
    """
778
    if not new_class.master or not isinstance(new_class.master, ForwardManyToOneDescriptor):
779
        raise ImproperlyConfigured("{0}.master should be a ForeignKey to the shared table.".format(new_class.__name__))
780
781
    rel = new_class.master.field.rel
782
    shared_model = rel.to
783
784
    if not issubclass(shared_model, models.Model):
785
        # Not supporting models.ForeignKey("tablename") yet. Can't use get_model() as the models are still being constructed.
786
        raise ImproperlyConfigured("{0}.master should point to a model class, can't use named field here.".format(new_class.__name__))
787
788
    meta = shared_model._parler_meta
789
    if meta is not None:
790
        if meta._has_translations_model(new_class):
791
            raise ImproperlyConfigured("The model '{0}' already has an associated translation table!".format(shared_model.__name__))
792
        if meta._has_translations_field(rel.related_name):
793
            raise ImproperlyConfigured("The model '{0}' already has an associated translation field named '{1}'!".format(shared_model.__name__, rel.related_name))
794
795
    return shared_model
796
797
798
@python_2_unicode_compatible
799
class TranslatedFieldsModel(compat.with_metaclass(TranslatedFieldsModelBase, models.Model)):
800
    """
801
    Base class for the model that holds the translated fields.
802
    """
803
    language_code = compat.HideChoicesCharField(_("Language"), choices=settings.LANGUAGES, max_length=15, db_index=True)
804
805
    #: The mandatory Foreign key field to the shared model.
806
    master = None   # FK to shared model.
807
808
    class Meta:
809
        abstract = True
810
        if django.VERSION >= (1, 7):
811
            default_permissions = ()
812
813
    def __init__(self, *args, **kwargs):
814
        signals.pre_translation_init.send(sender=self.__class__, args=args, kwargs=kwargs)
815
        super(TranslatedFieldsModel, self).__init__(*args, **kwargs)
816
        self._original_values = self._get_field_values()
817
818
        signals.post_translation_init.send(sender=self.__class__, args=args, kwargs=kwargs)
819
820
    @property
821
    def is_modified(self):
822
        """
823
        Tell whether the object content is modified since fetching it.
824
        """
825
        return self._original_values != self._get_field_values()
826
827
    @property
828
    def is_empty(self):
829
        """
830
        True when there are no translated fields.
831
        """
832
        return len(self.get_translated_fields()) == 0
833
834
    @property
835
    def shared_model(self):
836
        """
837
        Returns the shared model this model is linked to.
838
        """
839
        return self.__class__.master.field.rel.to
840
841
    @property
842
    def related_name(self):
843
        """
844
        Returns the related name that this model is known at in the shared model.
845
        """
846
        return self.__class__.master.field.rel.related_name
847
848
    def save_base(self, raw=False, using=None, **kwargs):
849
        # As of Django 1.8, not calling translations.activate() or disabling the translation
850
        # causes get_language() to explicitly return None instead of LANGUAGE_CODE.
851
        # This helps developers find solutions by bailing out properly.
852
        #
853
        # Either use translation.activate() first, or pass the language code explicitly via
854
        # MyModel.objects.language('en').create(..)
855
        assert self.language_code is not None, ""\
856
            "No language is set or detected for this TranslatableModelMixin.\n" \
857
            "Is the translations system initialized?"
858
859
        # Send the pre_save signal
860
        using = using or router.db_for_write(self.__class__, instance=self)
861
        record_exists = self.pk is not None  # Ignoring force_insert/force_update for now.
862
        if not self._meta.auto_created:
863
            signals.pre_translation_save.send(
864
                sender=self.shared_model, instance=self,
865
                raw=raw, using=using
866
            )
867
868
        # Perform save
869
        super(TranslatedFieldsModel, self).save_base(raw=raw, using=using, **kwargs)
870
        self._original_values = self._get_field_values()
871
        _cache_translation(self)
872
873
        # Send the post_save signal
874
        if not self._meta.auto_created:
875
            signals.post_translation_save.send(
876
                sender=self.shared_model, instance=self, created=(not record_exists),
877
                raw=raw, using=using
878
            )
879
880
    def delete(self, using=None):
881
        # Send pre-delete signal
882
        using = using or router.db_for_write(self.__class__, instance=self)
883
        if not self._meta.auto_created:
884
            signals.pre_translation_delete.send(sender=self.shared_model, instance=self, using=using)
885
886
        super(TranslatedFieldsModel, self).delete(using=using)
887
        _delete_cached_translation(self)
888
889
        # Send post-delete signal
890
        if not self._meta.auto_created:
891
            signals.post_translation_delete.send(sender=self.shared_model, instance=self, using=using)
892
893
    if django.VERSION >= (1, 8):
894
        def _get_field_values(self):
895
            # Use the new Model._meta API.
896
            return [getattr(self, field.get_attname()) for field in self._meta.get_fields() if not field.is_relation or field.many_to_one]
897
    else:
898
        def _get_field_values(self):
899
            # Return all field values in a consistent (sorted) manner.
900
            return [getattr(self, field.get_attname()) for field, _ in self._meta.get_fields_with_model()]
901
902
    @classmethod
903
    def get_translated_fields(cls):
904
        return [f.name for f in cls._meta.local_fields if f.name not in ('language_code', 'master', 'id')]
905
906
    @classmethod
907
    def contribute_translations(cls, shared_model):
908
        """
909
        Add the proxy attributes to the shared model.
910
        """
911
        # Instance at previous inheritance level, if set.
912
        base = shared_model._parler_meta
913
914
        if base is not None and base[-1].shared_model is shared_model:
915
            # If a second translations model is added, register it in the same object level.
916
            base.add_meta(ParlerMeta(
917
                shared_model=shared_model,
918
                translations_model=cls,
919
                related_name=cls.master.field.rel.related_name
920
            ))
921
        else:
922
            # Place a new _parler_meta at the current inheritance level.
923
            # It links to the previous base.
924
            shared_model._parler_meta = ParlerOptions(
925
                base,
926
                shared_model=shared_model,
927
                translations_model=cls,
928
                related_name=cls.master.field.rel.related_name
929
            )
930
931
        # Assign the proxy fields
932
        for name in cls.get_translated_fields():
933
            try:
934
                # Check if an attribute already exists.
935
                # Note that the descriptor even proxies this request, so it should return our field.
936
                #
937
                # A model field might not be added yet, as this all happens in the contribute_to_class() loop.
938
                # Hence, only checking attributes here. The real fields are checked for in the _prepare() code.
939
                shared_field = getattr(shared_model, name)
940
            except AttributeError:
941
                # Add the proxy field for the shared field.
942
                TranslatedField().contribute_to_class(shared_model, name)
943
            else:
944
                # Currently not allowing to replace existing model fields with translatable fields.
945
                # That would be a nice feature addition however.
946
                if not isinstance(shared_field, (models.Field, TranslatedFieldDescriptor)):
947
                    raise TypeError("The model '{0}' already has a field named '{1}'".format(shared_model.__name__, name))
948
949
                # When the descriptor was placed on an abstract model,
950
                # it doesn't point to the real model that holds the translations_model
951
                # "Upgrade" the descriptor on the class
952
                if shared_field.field.model is not shared_model:
953
                    TranslatedField(any_language=shared_field.field.any_language).contribute_to_class(shared_model, name)
954
955
        # Make sure the DoesNotExist error can be detected als shared_model.DoesNotExist too,
956
        # and by inheriting from AttributeError it makes sure (admin) templates can handle the missing attribute.
957
        cls.DoesNotExist = type(str('DoesNotExist'), (TranslationDoesNotExist, shared_model.DoesNotExist, cls.DoesNotExist,), {})
958
959
    def __str__(self):
960
        # use format to avoid weird error in django 1.4
961
        # TypeError: coercing to Unicode: need string or buffer, __proxy__ found
962
        return "{0}".format(get_language_title(self.language_code))
963
964
    def __repr__(self):
965
        return "<{0}: #{1}, {2}, master: #{3}>".format(
966
            self.__class__.__name__, self.pk, self.language_code, self.master_id
967
        )
968
969
970
class ParlerMeta(object):
971
    """
972
    Meta data for a single inheritance level.
973
    """
974
975
    def __init__(self, shared_model, translations_model, related_name):
976
        # Store meta information of *this* level
977
        self.shared_model = shared_model
978
        self.model = translations_model
979
        self.rel_name = related_name
980
981
    def get_translated_fields(self):
982
        """
983
        Return the translated fields of this model.
984
        """
985
        # TODO: should be named get_fields() ?
986
        # root_model always points to the real model for extensions
987
        return self.model.get_translated_fields()
988
989
    def __repr__(self):
990
        return "<ParlerMeta: {0}.{1} to {2}>".format(
991
            self.shared_model.__name__,
992
            self.rel_name,
993
            self.model.__name__
994
        )
995
996
997
class ParlerOptions(object):
998
    """
999
    Meta data for the translatable models.
1000
    """
1001
1002
    def __init__(self, base, shared_model, translations_model, related_name):
1003
        if translations_model is None is not issubclass(translations_model, TranslatedFieldsModel):
1004
            raise TypeError("Expected a TranslatedFieldsModel")
1005
1006
        self.base = base
1007
        self.inherited = False
1008
1009
        if base is None:
1010
            # Make access easier.
1011
            self.root_model = translations_model
1012
            self.root_rel_name = related_name
1013
1014
            # Initial state for lookups
1015
            self._root = None
1016
            self._extensions = []
1017
            self._fields_to_model = OrderedDict()
1018
        else:
1019
            # Inherited situation
1020
            # Still take the base situation as starting point,
1021
            # and register the added translations as extension.
1022
            root = base._root or base
1023
            base.inherited = True
1024
            self._root = root
1025
            self.root_model = root.root_model
1026
            self.root_rel_name = root.root_rel_name
1027
1028
            # This object will amend the caches of the previous object
1029
            # The _extensions list gives access to all inheritance levels where ParlerOptions is defined.
1030
            self._extensions = list(base._extensions)
1031
            self._fields_to_model = base._fields_to_model.copy()
1032
1033
        self.add_meta(ParlerMeta(shared_model, translations_model, related_name))
1034
1035
    def add_meta(self, meta):
1036
        if self.inherited:
1037
            raise RuntimeError("Adding translations afterwards to an already inherited model is not supported yet.")
1038
1039
        self._extensions.append(meta)
1040
1041
        # Fill/amend the caches
1042
        translations_model = meta.model
1043
        for name in translations_model.get_translated_fields():
1044
            self._fields_to_model[name] = translations_model
1045
1046
    def __repr__(self):
1047
        root = self.root
1048
        return "<ParlerOptions: {0}.{1} to {2}{3}>".format(
1049
            root.shared_model.__name__,
1050
            root.rel_name,
1051
            root.model.__name__,
1052
            '' if len(self._extensions) == 1 else ", {0} extensions".format(len(self._extensions))
1053
        )
1054
1055
    @property
1056
    def root(self):
1057
        """
1058
        The top level object in the inheritance chain.
1059
        This is an alias for accessing the first item in the collection.
1060
        """
1061
        return self._extensions[0]
1062
1063
    def __iter__(self):
1064
        """
1065
        Access all :class:`ParlerMeta` objects associated.
1066
        """
1067
        return iter(self._extensions)
1068
1069
    def __getitem__(self, item):
1070
        """
1071
        Get an :class:`ParlerMeta` object by index or model.
1072
        """
1073
        try:
1074
            if isinstance(item, six.integer_types):
1075
                return self._extensions[item]
1076
            elif isinstance(item, six.string_types):
1077
                return self._get_extension_by_related_name(related_name=item)
1078
            else:
1079
                return next(meta for meta in self._extensions if meta.model == item)
1080
        except (StopIteration, IndexError, KeyError):
1081
            raise KeyError("Item '{0}' not found".format(item))
1082
1083
    def __len__(self):
1084
        return len(self._extensions)
1085
1086
    def get_all_models(self):
1087
        """
1088
        Return all translated models associated with the the shared model.
1089
        """
1090
        return [meta.model for meta in self._extensions]
1091
1092
    def get_all_fields(self):
1093
        """
1094
        Return all translated fields associated with this model.
1095
        """
1096
        return list(self._fields_to_model.keys())
1097
1098
    def get_fields_with_model(self):
1099
        """
1100
        Convenience function, return all translated fields with their model.
1101
        """
1102
        return six.iteritems(self._fields_to_model)
1103
1104
    def get_translated_fields(self, related_name=None):
1105
        """
1106
        Return the translated fields of this model.
1107
        By default, the top-level translation is required, unless ``related_name`` is provided.
1108
        """
1109
        # TODO: should be named get_fields() ?
1110
        meta = self._get_extension_by_related_name(related_name)
1111
        return meta.get_translated_fields()
1112
1113
    def get_model_by_field(self, name):
1114
        """
1115
        Find the :class:`TranslatedFieldsModel` that contains the given field.
1116
        """
1117
        try:
1118
            return self._fields_to_model[name]
1119
        except KeyError:
1120
            raise FieldError("Translated field does not exist: '{0}'".format(name))
1121
1122
    def get_model_by_related_name(self, related_name):
1123
        meta = self._get_extension_by_related_name(related_name)
1124
        return meta.model  # extensions have no base set, so root model is correct here.
1125
1126
    def _has_translations_model(self, model):
1127
        return any(meta.model == model for meta in self._extensions)
1128
1129
    def _has_translations_field(self, name):
1130
        return any(meta.rel_name == name for meta in self._extensions)
1131
1132
    def _get_extension_by_field(self, name):
1133
        """
1134
        Find the ParlerOptions object that corresponds with the given translated field.
1135
        """
1136
        if name is None:
1137
            raise TypeError("Expected field name")
1138
1139
        # Reuse existing lookups.
1140
        tr_model = self.get_model_by_field(name)
1141
        for meta in self._extensions:
1142
            if meta.model == tr_model:
1143
                return meta
1144
1145
    def _get_extension_by_related_name(self, related_name):
1146
        """
1147
        Find which model is connected to a given related name.
1148
        If the related name is ``None``, the :attr:`root_model` will be returned.
1149
        """
1150
        if related_name is None:
1151
            return self._extensions[0]
1152
1153
        for meta in self._extensions:
1154
            if meta.rel_name == related_name:
1155
                return meta
1156
1157
        raise ValueError("No translated model of '{0}' has a reverse name of '{1}'".format(
1158
            self.root.shared_model.__name__, related_name
1159
        ))
1160
1161
    def _split_fields(self, **fields):
1162
        # Split fields over their translated models.
1163
        for meta in self._extensions:
1164
            model_fields = {}
1165
            for field in meta.model.get_translated_fields():
1166
                try:
1167
                    model_fields[field] = fields[field]
1168
                except KeyError:
1169
                    pass
1170
1171
            yield (meta, model_fields)
1172