/
translation.py
403 lines (332 loc) · 16.3 KB
/
translation.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
"""
Support for models' internal Translation class.
"""
## TO DO: this is messy and needs to be cleaned up
from django.contrib.admin import StackedInline, ModelAdmin
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import signals
from django.db.models.base import ModelBase
from django.dispatch.dispatcher import connect
from multilingual.languages import *
from multilingual.exceptions import TranslationDoesNotExist
from multilingual.fields import TranslationForeignKey
from multilingual.manipulators import add_multilingual_manipulators
from multilingual import manager
from new import instancemethod
class TranslationModelAdmin(StackedInline):
template = "admin/edit_inline_translations_newforms.html"
def translation_save_translated_fields(instance, **kwargs):
"""
Save all the translations of instance in post_save signal handler.
"""
if not hasattr(instance, '_translation_cache'):
return
for l_id, translation in instance._translation_cache.iteritems():
# set the translation ID just in case the translation was
# created while instance was not stored in the DB yet
# note: we're using _get_pk_val here even though it is
# private, since that's the most reliable way to get the value
# on older Django (pk property did not exist yet)
translation.master_id = instance._get_pk_val()
translation.save()
def translation_overwrite_previous(instance, **kwargs):
"""
Delete previously existing translation with the same master and
language_id values. To be called by translation model's pre_save
signal.
This most probably means I am abusing something here trying to use
Django inline editor. Oh well, it will have to go anyway when we
move to newforms.
"""
qs = instance.__class__.objects
try:
qs = qs.filter(master=instance.master).filter(language_id=instance.language_id)
qs.delete()
except ObjectDoesNotExist:
# We are probably loading a fixture that defines translation entities
# before their master entity.
pass
def fill_translation_cache(instance):
"""
Fill the translation cache using information received in the
instance objects as extra fields.
You can not do this in post_init because the extra fields are
assigned by QuerySet.iterator after model initialization.
"""
if hasattr(instance, '_translation_cache'):
# do not refill the cache
return
instance._translation_cache = {}
for language_id in get_language_id_list():
# see if translation for language_id was in the query
field_alias = get_translated_field_alias('id', language_id)
if getattr(instance, field_alias, None) is not None:
field_names = [f.attname for f in instance._meta.translation_model._meta.fields]
# if so, create a translation object and put it in the cache
field_data = {}
for fname in field_names:
field_data[fname] = getattr(instance,
get_translated_field_alias(fname, language_id))
translation = instance._meta.translation_model(**field_data)
instance._translation_cache[language_id] = translation
# In some situations an (existing in the DB) object is loaded
# without using the normal QuerySet. In such case fallback to
# loading the translations using a separate query.
# Unfortunately, this is indistinguishable from the situation when
# an object does not have any translations. Oh well, we'll have
# to live with this for the time being.
if len(instance._translation_cache.keys()) == 0:
for translation in instance.translations.all():
instance._translation_cache[translation.language_id] = translation
class TranslatedFieldProxy(property):
def __init__(self, field_name, alias, field, language_id=None):
self.field_name = field_name
self.field = field
self.admin_order_field = alias
self.language_id = language_id
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, 'get_' + self.field_name)(self.language_id)
def __set__(self, obj, value):
language_id = self.language_id
return getattr(obj, 'set_' + self.field_name)(value, self.language_id)
short_description = property(lambda self: self.field.short_description)
def getter_generator(field_name, short_description):
"""
Generate get_'field name' method for field field_name.
"""
def get_translation_field(self, language_id_or_code=None):
try:
return getattr(self.get_translation(language_id_or_code), field_name)
except TranslationDoesNotExist:
return None
get_translation_field.short_description = short_description
return get_translation_field
def setter_generator(field_name):
"""
Generate set_'field name' method for field field_name.
"""
def set_translation_field(self, value, language_id_or_code=None):
setattr(self.get_translation(language_id_or_code, True),
field_name, value)
set_translation_field.short_description = "set " + field_name
return set_translation_field
def get_translation(self, language_id_or_code,
create_if_necessary=False):
"""
Get a translation instance for the given language_id_or_code.
If it does not exist, either create one or raise the
TranslationDoesNotExist exception, depending on the
create_if_necessary argument.
"""
# fill the cache if necessary
self.fill_translation_cache()
language_id = get_language_id_from_id_or_code(language_id_or_code, False)
if language_id is None:
language_id = getattr(self, '_default_language', None)
if language_id is None:
language_id = get_default_language()
if language_id not in self._translation_cache:
if not create_if_necessary:
raise TranslationDoesNotExist(language_id)
new_translation = self._meta.translation_model(master=self,
language_id=language_id)
self._translation_cache[language_id] = new_translation
return self._translation_cache.get(language_id, None)
class Translation:
"""
A superclass for translatablemodel.Translation inner classes.
"""
def contribute_to_class(cls, main_cls, name):
"""
Handle the inner 'Translation' class.
"""
# delay the creation of the *Translation until the master model is
# fully created
connect(cls.finish_multilingual_class, signal=signals.class_prepared,
sender=main_cls, weak=False)
# connect the post_save signal on master class to a handler
# that saves translations
connect(translation_save_translated_fields, signal=signals.post_save,
sender=main_cls)
contribute_to_class = classmethod(contribute_to_class)
def create_translation_attrs(cls, main_cls):
"""
Creates get_'field name'(language_id) and set_'field
name'(language_id) methods for all the translation fields.
Adds the 'field name' properties too.
Returns the translated_fields hash used in field lookups, see
multilingual.query. It maps field names to (field,
language_id) tuples.
"""
translated_fields = {}
for fname, field in cls.__dict__.items():
if isinstance(field, models.fields.Field):
translated_fields[fname] = (field, None)
# add get_'fname' and set_'fname' methods to main_cls
getter = getter_generator(fname, getattr(field, 'verbose_name', fname))
setattr(main_cls, 'get_' + fname, getter)
setter = setter_generator(fname)
setattr(main_cls, 'set_' + fname, setter)
# add the 'fname' proxy property that allows reads
# from and writing to the appropriate translation
setattr(main_cls, fname,
TranslatedFieldProxy(fname, fname, field))
# create the 'fname'_'language_code' proxy properties
for language_id in get_language_id_list():
language_code = get_language_code(language_id)
fname_lng = fname + '_' + language_code.replace('-', '_')
translated_fields[fname_lng] = (field, language_id)
setattr(main_cls, fname_lng,
TranslatedFieldProxy(fname, fname_lng, field,
language_id))
return translated_fields
create_translation_attrs = classmethod(create_translation_attrs)
def get_unique_fields(cls):
"""
Return a list of fields with "unique" attribute, which needs to
be augmented by the language.
"""
unique_fields = []
for fname, field in cls.__dict__.items():
if isinstance(field, models.fields.Field):
if getattr(field,'unique',False):
try:
field.unique = False
except AttributeError:
# newer Django defines unique as a property
# that uses _unique to store data. We're
# jumping over the fence by setting _unique,
# so this sucks, but this happens early enough
# to be safe.
field._unique = False
unique_fields.append(fname)
return unique_fields
get_unique_fields = classmethod(get_unique_fields)
def finish_multilingual_class(cls, *args, **kwargs):
"""
Create a model with translations of a multilingual class.
"""
main_cls = kwargs['sender']
translation_model_name = main_cls.__name__ + "Translation"
# create the model with all the translatable fields
unique = [('language_id', 'master')]
for f in cls.get_unique_fields():
unique.append(('language_id',f))
class TransMeta:
pass
try:
meta = cls.Meta
except AttributeError:
meta = TransMeta
meta.ordering = ('language_id',)
meta.unique_together = tuple(unique)
meta.app_label = main_cls._meta.app_label
if not hasattr(meta, 'db_table'):
meta.db_table = main_cls._meta.db_table + '_translation'
trans_attrs = cls.__dict__.copy()
trans_attrs['Meta'] = meta
trans_attrs['language_id'] = models.IntegerField(blank=False, null=False, core=True,
choices=get_language_choices(),
db_index=True)
edit_inline = True
trans_attrs['master'] = TranslationForeignKey(main_cls, blank=False, null=False,
edit_inline=edit_inline,
related_name='translations',
num_in_admin=get_language_count(),
min_num_in_admin=get_language_count(),
num_extra_on_change=0)
trans_attrs['__str__'] = lambda self: ("%s object, language_code=%s"
% (translation_model_name,
get_language_code(self.language_id)))
trans_model = ModelBase(translation_model_name, (models.Model,), trans_attrs)
trans_model._meta.translated_fields = cls.create_translation_attrs(main_cls)
_old_init_name_map = main_cls._meta.__class__.init_name_map
def init_name_map(self):
cache = _old_init_name_map(self)
for name, field_and_lang_id in trans_model._meta.translated_fields.items():
#import sys; sys.stderr.write('TM %r\n' % trans_model)
cache[name] = (field_and_lang_id[0], trans_model, True, False)
return cache
main_cls._meta.init_name_map = instancemethod(init_name_map,
main_cls._meta,
main_cls._meta.__class__)
main_cls._meta.translation_model = trans_model
main_cls.get_translation = get_translation
main_cls.fill_translation_cache = fill_translation_cache
# Note: don't fill the translation cache in post_init, as all
# the extra values selected by QAddTranslationData will be
# assigned AFTER init()
# connect(fill_translation_cache, signal=signals.post_init,
# sender=main_cls)
# connect the pre_save signal on translation class to a
# function removing previous translation entries.
connect(translation_overwrite_previous, signal=signals.pre_save,
sender=trans_model, weak=False)
finish_multilingual_class = classmethod(finish_multilingual_class)
def install_translation_library():
# modify ModelBase.__new__ so that it understands how to handle the
# 'Translation' inner class
if getattr(ModelBase, '_multilingual_installed', False):
# don't install it twice
return
_old_new = ModelBase.__new__
def multilingual_modelbase_new(cls, name, bases, attrs):
if 'Translation' in attrs:
if not issubclass(attrs['Translation'], Translation):
raise ValueError, ("%s.Translation must be a subclass "
+ " of multilingual.Translation.") % (name,)
# Make sure that if the class specifies objects then it is
# a subclass of our Manager.
#
# Don't check other managers since someone might want to
# have a non-multilingual manager, but assigning a
# non-multilingual manager to objects would be a common
# mistake.
if ('objects' in attrs) and (not isinstance(attrs['objects'], manager.Manager)):
raise ValueError, ("Model %s specifies translations, " +
"so its 'objects' manager must be " +
"a subclass of multilingual.Manager.") % (name,)
# Change the default manager to multilingual.Manager.
if not 'objects' in attrs:
attrs['objects'] = manager.Manager()
# Install a hack to let add_multilingual_manipulators know
# this is a translatable model
attrs['is_translation_model'] = lambda self: True
return _old_new(cls, name, bases, attrs)
ModelBase.__new__ = staticmethod(multilingual_modelbase_new)
ModelBase._multilingual_installed = True
# Override ModelAdmin.__new__ to create automatic inline
# editor for multilingual models.
_old_admin_new = ModelAdmin.__new__
def multilingual_modeladmin_new(cls, model, admin_site):
if isinstance(model.objects, manager.Manager):
X = cls.get_translation_modeladmin(model)
if cls.inlines:
for inline in cls.inlines:
if X.__class__ == inline.__class__:
cls.inlines.remove(inline)
break
cls.inlines.append(X)
else:
cls.inlines = [X]
return _old_admin_new(cls, model, admin_site)
def get_translation_modeladmin(cls, model):
if hasattr(cls, 'Translation'):
tr_cls = cls.Translation
if not issubclass(tr_cls, TranslationModelAdmin):
raise ValueError, ("%s.Translation must be a subclass "
+ " of multilingual.TranslationModelAdmin.") % cls.name
else:
tr_cls = TranslationModelAdmin
tr_cls.model = model._meta.translation_model
tr_cls.fk_name = 'master'
tr_cls.extra = get_language_count()
tr_cls.max_num = get_language_count()
return tr_cls
ModelAdmin.__new__ = staticmethod(multilingual_modeladmin_new)
ModelAdmin.get_translation_modeladmin = classmethod(get_translation_modeladmin)
# install the library
install_translation_library()