1
|
|
|
""" |
2
|
|
|
django-parler uses caching to avoid fetching model data when it doesn't have to. |
3
|
|
|
|
4
|
|
|
These functions are used internally by django-parler to fetch model data. |
5
|
|
|
Since all calls to the translation table are routed through our model descriptor fields, |
6
|
|
|
cache access and expiry is rather simple to implement. |
7
|
|
|
""" |
8
|
|
|
import django |
9
|
|
|
from django.core.cache import cache |
10
|
|
|
from django.utils import six |
11
|
|
|
from parler import appsettings |
12
|
|
|
from parler.utils import get_language_settings |
13
|
|
|
|
14
|
|
|
if six.PY3: |
15
|
|
|
long = int |
16
|
|
|
|
17
|
|
|
if django.VERSION >= (1, 6): |
18
|
|
|
DEFAULT_TIMEOUT = cache.default_timeout |
19
|
|
|
else: |
20
|
|
|
DEFAULT_TIMEOUT = 0 |
21
|
|
|
|
22
|
|
|
|
23
|
|
|
class IsMissing(object): |
24
|
|
|
# Allow _get_any_translated_model() to evaluate this as False. |
25
|
|
|
|
26
|
|
|
def __nonzero__(self): |
27
|
|
|
return False # Python 2 |
28
|
|
|
|
29
|
|
|
def __bool__(self): |
30
|
|
|
return False # Python 3 |
31
|
|
|
|
32
|
|
|
def __repr__(self): |
33
|
|
|
return "<IsMissing>" |
34
|
|
|
|
35
|
|
|
MISSING = IsMissing() # sentinel value |
36
|
|
|
|
37
|
|
|
|
38
|
|
|
def get_object_cache_keys(instance): |
39
|
|
|
""" |
40
|
|
|
Return the cache keys associated with an object. |
41
|
|
|
""" |
42
|
|
|
if instance.pk is None or instance._state.adding: |
43
|
|
|
return [] |
44
|
|
|
|
45
|
|
|
keys = [] |
46
|
|
|
tr_models = instance._parler_meta.get_all_models() |
47
|
|
|
|
48
|
|
|
# TODO: performs a query to fetch the language codes. Store that in memcached too. |
49
|
|
|
for language in instance.get_available_languages(): |
50
|
|
|
for tr_model in tr_models: |
51
|
|
|
keys.append(get_translation_cache_key(tr_model, instance.pk, language)) |
52
|
|
|
|
53
|
|
|
return keys |
54
|
|
|
|
55
|
|
|
|
56
|
|
|
def get_translation_cache_key(translated_model, master_id, language_code): |
57
|
|
|
""" |
58
|
|
|
The low-level function to get the cache key for a translation. |
59
|
|
|
""" |
60
|
|
|
# Always cache the entire object, as this already produces |
61
|
|
|
# a lot of queries. Don't go for caching individual fields. |
62
|
|
|
return 'parler.{0}.{1}.{2}.{3}'.format(translated_model._meta.app_label, translated_model.__name__, master_id, language_code) |
63
|
|
|
|
64
|
|
|
|
65
|
|
|
def get_cached_translation(instance, language_code=None, related_name=None, use_fallback=False): |
66
|
|
|
""" |
67
|
|
|
Fetch an cached translation. |
68
|
|
|
|
69
|
|
|
.. versionadded 1.2 Added the ``related_name`` parameter. |
70
|
|
|
""" |
71
|
|
|
if language_code is None: |
72
|
|
|
language_code = instance.get_current_language() |
73
|
|
|
|
74
|
|
|
translated_model = instance._parler_meta.get_model_by_related_name(related_name) |
75
|
|
|
values = _get_cached_values(instance, translated_model, language_code, use_fallback) |
76
|
|
|
if not values: |
77
|
|
|
return None |
78
|
|
|
|
79
|
|
|
try: |
80
|
|
|
translation = translated_model(**values) |
81
|
|
|
except TypeError: |
82
|
|
|
# Some model field was removed, cache entry is no longer working. |
83
|
|
|
return None |
84
|
|
|
|
85
|
|
|
translation._state.adding = False |
86
|
|
|
return translation |
87
|
|
|
|
88
|
|
|
|
89
|
|
|
def get_cached_translated_field(instance, field_name, language_code=None, use_fallback=False): |
90
|
|
|
""" |
91
|
|
|
Fetch an cached field. |
92
|
|
|
""" |
93
|
|
|
if language_code is None: |
94
|
|
|
language_code = instance.get_current_language() |
95
|
|
|
|
96
|
|
|
# In django-parler 1.1 the order of the arguments was fixed, It used to be language_code, field_name |
97
|
|
|
# This serves as detection against backwards incompatibility issues. |
98
|
|
|
if len(field_name) <= 5 and len(language_code) > 5: |
99
|
|
|
raise RuntimeError("Unexpected language code, did you swap field_name, language_code?") |
100
|
|
|
|
101
|
|
|
translated_model = instance._parler_meta.get_model_by_field(field_name) |
102
|
|
|
values = _get_cached_values(instance, translated_model, language_code, use_fallback) |
103
|
|
|
if not values: |
104
|
|
|
return None |
105
|
|
|
|
106
|
|
|
# Allow older cached versions where the field didn't exist yet. |
107
|
|
|
return values.get(field_name, None) |
108
|
|
|
|
109
|
|
|
|
110
|
|
|
def _get_cached_values(instance, translated_model, language_code, use_fallback=False): |
111
|
|
|
""" |
112
|
|
|
Fetch an cached field. |
113
|
|
|
""" |
114
|
|
|
if not appsettings.PARLER_ENABLE_CACHING or not instance.pk or instance._state.adding: |
115
|
|
|
return None |
116
|
|
|
|
117
|
|
|
key = get_translation_cache_key(translated_model, instance.pk, language_code) |
118
|
|
|
values = cache.get(key) |
119
|
|
|
if not values: |
120
|
|
|
return None |
121
|
|
|
|
122
|
|
|
# Check for a stored fallback marker |
123
|
|
|
if values.get('__FALLBACK__', False): |
124
|
|
|
# Internal trick, already set the fallback marker, so no query will be performed. |
125
|
|
|
instance._translations_cache[translated_model][language_code] = MISSING |
126
|
|
|
|
127
|
|
|
# Allow to return the fallback language instead. |
128
|
|
|
if use_fallback: |
129
|
|
|
lang_dict = get_language_settings(language_code) |
130
|
|
|
# iterate over list of fallback languages, which should be already |
131
|
|
|
# in proper order |
132
|
|
|
for fallback_lang in lang_dict['fallbacks']: |
133
|
|
|
if fallback_lang != language_code: |
134
|
|
|
return _get_cached_values( |
135
|
|
|
instance, translated_model, fallback_lang, |
136
|
|
|
use_fallback=False |
137
|
|
|
) |
138
|
|
|
return None |
139
|
|
|
|
140
|
|
|
values['master'] = instance |
141
|
|
|
values['language_code'] = language_code |
142
|
|
|
return values |
143
|
|
|
|
144
|
|
|
|
145
|
|
|
def _cache_translation(translation, timeout=DEFAULT_TIMEOUT): |
146
|
|
|
""" |
147
|
|
|
Store a new translation in the cache. |
148
|
|
|
""" |
149
|
|
|
if not appsettings.PARLER_ENABLE_CACHING: |
150
|
|
|
return |
151
|
|
|
|
152
|
|
|
if translation.master_id is None: |
153
|
|
|
raise ValueError("Can't cache unsaved translation") |
154
|
|
|
|
155
|
|
|
# Cache a translation object. |
156
|
|
|
# For internal usage, object parameters are not suited for outside usage. |
157
|
|
|
fields = translation.get_translated_fields() |
158
|
|
|
values = {'id': translation.id} |
159
|
|
|
for name in fields: |
160
|
|
|
values[name] = getattr(translation, name) |
161
|
|
|
|
162
|
|
|
key = get_translation_cache_key(translation.__class__, translation.master_id, translation.language_code) |
163
|
|
|
cache.set(key, values, timeout=timeout) |
164
|
|
|
|
165
|
|
|
|
166
|
|
|
def _cache_translation_needs_fallback(instance, language_code, related_name, timeout=DEFAULT_TIMEOUT): |
167
|
|
|
""" |
168
|
|
|
Store the fact that a translation doesn't exist, and the fallback should be used. |
169
|
|
|
""" |
170
|
|
|
if not appsettings.PARLER_ENABLE_CACHING or not instance.pk or instance._state.adding: |
171
|
|
|
return |
172
|
|
|
|
173
|
|
|
tr_model = instance._parler_meta.get_model_by_related_name(related_name) |
174
|
|
|
key = get_translation_cache_key(tr_model, instance.pk, language_code) |
175
|
|
|
cache.set(key, {'__FALLBACK__': True}, timeout=timeout) |
176
|
|
|
|
177
|
|
|
|
178
|
|
|
def _delete_cached_translations(shared_model): |
179
|
|
|
cache.delete_many(get_object_cache_keys(shared_model)) |
180
|
|
|
|
181
|
|
|
|
182
|
|
|
def _delete_cached_translation(translation): |
183
|
|
|
if not appsettings.PARLER_ENABLE_CACHING: |
184
|
|
|
return |
185
|
|
|
|
186
|
|
|
# Delete a cached translation |
187
|
|
|
# For internal usage, object parameters are not suited for outside usage. |
188
|
|
|
key = get_translation_cache_key(translation.__class__, translation.master_id, translation.language_code) |
189
|
|
|
cache.delete(key) |
190
|
|
|
|