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