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 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333
|
"""
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:
signals.post_translation_delete.send(
sender=self.shared_model, instance=self, using=using
)
def _get_field_names(self):
# Use the new Model._meta API.
return [
field.get_attname()
for field in self._meta.get_fields()
if not field.is_relation or field.many_to_one
]
def _get_field_values(self):
# Use the new Model._meta API.
return [
getattr(self, field.get_attname())
for field in self._meta.get_fields()
if not field.is_relation or field.many_to_one
]
@classmethod
def get_translated_fields(cls, include_m2m=True):
res = [
f.name
for f in cls._meta.local_fields
if f.name not in ("language_code", "master", "id")
]
if include_m2m:
res += [
f.name
for f in cls._meta.local_many_to_many
if f.name not in ("language_code", "master", "id")
]
return res
@classmethod
def contribute_translations(cls, shared_model):
"""
Add the proxy attributes to the shared model.
"""
# Instance at previous inheritance level, if set.
# This is checked for None as some migration files don't use bases=TranslatableModel instead
try:
base = shared_model._parler_meta
except AttributeError:
raise TypeError(
f"Translatable model {shared_model} does not appear to inherit from TranslatableModel"
)
if base is not None and base[-1].shared_model is shared_model:
# If a second translations model is added, register it in the same object level.
base.add_meta(
ParlerMeta(
shared_model=shared_model,
translations_model=cls,
related_name=cls.master.field.remote_field.related_name,
)
)
else:
# Place a new _parler_meta at the current inheritance level.
# It links to the previous base.
shared_model._parler_meta = ParlerOptions(
base,
shared_model=shared_model,
translations_model=cls,
related_name=cls.master.field.remote_field.related_name,
)
# Assign the proxy fields
for name in cls.get_translated_fields():
try:
# Check if an attribute already exists.
# Note that the descriptor even proxies this request, so it should return our field.
#
# A model field might not be added yet, as this all happens in the contribute_to_class() loop.
# Hence, only checking attributes here. The real fields are checked for in the _prepare() code.
shared_field = getattr(shared_model, name)
except AttributeError:
# Add the proxy field for the shared field.
TranslatedField().contribute_to_class(shared_model, name)
else:
# Currently not allowing to replace existing model fields with translatable fields.
# That would be a nice feature addition however.
if not isinstance(shared_field, (models.Field, TranslatedFieldDescriptor)):
raise TypeError(
f"The model '{shared_model.__name__}' already has a field named '{name}'"
)
# When the descriptor was placed on an abstract model,
# it doesn't point to the real model that holds the translations_model
# "Upgrade" the descriptor on the class
if shared_field.field.model is not shared_model:
TranslatedField(
any_language=shared_field.field.any_language
).contribute_to_class(shared_model, name)
# Make sure the DoesNotExist error can be detected als shared_model.DoesNotExist too,
# and by inheriting from AttributeError it makes sure (admin) templates can handle the missing attribute.
cls.DoesNotExist = type(
"DoesNotExist",
(
TranslationDoesNotExist,
shared_model.DoesNotExist,
cls.DoesNotExist,
),
{},
)
def __str__(self):
return force_str(get_language_title(self.language_code))
def __repr__(self):
return "<{}: #{}, {}, master: #{}>".format(
self.__class__.__name__, self.pk, self.language_code, self.master_id
)
class TranslatedFieldsModel(
TranslatedFieldsModelMixin, models.Model, metaclass=TranslatedFieldsModelBase
):
language_code = compat.HideChoicesCharField(
_("Language"), choices=settings.LANGUAGES, max_length=15, db_index=True
)
class Meta:
abstract = True
default_permissions = ()
class ParlerMeta:
"""
Meta data for a single inheritance level.
"""
def __init__(self, shared_model, translations_model, related_name):
# Store meta information of *this* level
self.shared_model = shared_model
self.model = translations_model
self.rel_name = related_name
def get_translated_fields(self, include_m2m=True):
"""
Return the translated fields of this model.
"""
# TODO: should be named get_fields() ?
# root_model always points to the real model for extensions
return self.model.get_translated_fields(include_m2m=include_m2m)
def __repr__(self):
return "<ParlerMeta: {}.{} to {}>".format(
self.shared_model.__name__, self.rel_name, self.model.__name__
)
class ParlerOptions:
"""
Meta data for the translatable models.
"""
def __init__(self, base, shared_model, translations_model, related_name):
if translations_model is None is not issubclass(translations_model, TranslatedFieldsModel):
raise TypeError("Expected a TranslatedFieldsModel")
self.base = base
self.inherited = False
if base is None:
# Make access easier.
self.root_model = translations_model
self.root_rel_name = related_name
# Initial state for lookups
self._root = None
self._extensions = []
self._fields_to_model = OrderedDict()
else:
# Inherited situation
# Still take the base situation as starting point,
# and register the added translations as extension.
root = base._root or base
base.inherited = True
self._root = root
self.root_model = root.root_model
self.root_rel_name = root.root_rel_name
# This object will amend the caches of the previous object
# The _extensions list gives access to all inheritance levels where ParlerOptions is defined.
self._extensions = list(base._extensions)
self._fields_to_model = base._fields_to_model.copy()
self.add_meta(ParlerMeta(shared_model, translations_model, related_name))
def add_meta(self, meta):
if self.inherited:
raise RuntimeError(
"Adding translations afterwards to an already inherited model is not supported yet."
)
self._extensions.append(meta)
# Fill/amend the caches
translations_model = meta.model
for name in translations_model.get_translated_fields():
self._fields_to_model[name] = translations_model
def __repr__(self):
root = self.root
return "<ParlerOptions: {}.{} to {}{}>".format(
root.shared_model.__name__,
root.rel_name,
root.model.__name__,
"" if len(self._extensions) == 1 else f", {len(self._extensions)} extensions",
)
@property
def root(self):
"""
The top level object in the inheritance chain.
This is an alias for accessing the first item in the collection.
"""
return self._extensions[0]
def __iter__(self):
"""
Access all :class:`ParlerMeta` objects associated.
"""
return iter(self._extensions)
def __getitem__(self, item):
"""
Get an :class:`ParlerMeta` object by index or model.
"""
try:
if isinstance(item, int):
return self._extensions[item]
elif isinstance(item, str):
return self._get_extension_by_related_name(related_name=item)
else:
return next(meta for meta in self._extensions if meta.model == item)
except (StopIteration, IndexError, KeyError):
raise KeyError(f"Item '{item}' not found")
def __len__(self):
return len(self._extensions)
def get_all_models(self):
"""
Return all translated models associated with the the shared model.
"""
return [meta.model for meta in self._extensions]
def get_all_fields(self):
"""
Return all translated fields associated with this model.
"""
return list(self._fields_to_model.keys())
def get_fields_with_model(self):
"""
Convenience function, return all translated fields with their model.
"""
return self._fields_to_model.items()
def get_translated_fields(self, related_name=None, include_m2m=True):
"""
Return the translated fields of this model.
By default, the top-level translation is required, unless ``related_name`` is provided.
"""
# TODO: should be named get_fields() ?
meta = self._get_extension_by_related_name(related_name)
return meta.get_translated_fields(include_m2m=include_m2m)
def get_model_by_field(self, name):
"""
Find the :class:`TranslatedFieldsModel` that contains the given field.
"""
try:
return self._fields_to_model[name]
except KeyError:
raise FieldError(f"Translated field does not exist: '{name}'")
def get_model_by_related_name(self, related_name):
meta = self._get_extension_by_related_name(related_name)
return meta.model # extensions have no base set, so root model is correct here.
def _has_translations_model(self, model):
return any(meta.model == model for meta in self._extensions)
def _has_translations_field(self, name):
return any(meta.rel_name == name for meta in self._extensions)
def _get_extension_by_field(self, name):
"""
Find the ParlerOptions object that corresponds with the given translated field.
"""
if name is None:
raise TypeError("Expected field name")
# Reuse existing lookups.
tr_model = self.get_model_by_field(name)
for meta in self._extensions:
if meta.model == tr_model:
return meta
def _get_extension_by_related_name(self, related_name):
"""
Find which model is connected to a given related name.
If the related name is ``None``, the :attr:`root_model` will be returned.
"""
if related_name is None:
return self._extensions[0]
for meta in self._extensions:
if meta.rel_name == related_name:
return meta
raise ValueError(
"No translated model of '{}' has a reverse name of '{}'".format(
self.root.shared_model.__name__, related_name
)
)
def _split_fields(self, **fields):
# Split fields over their translated models.
for meta in self._extensions:
model_fields = {}
for field in meta.model.get_translated_fields():
try:
model_fields[field] = fields[field]
except KeyError:
pass
yield (meta, model_fields)
|