-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmixins.py
262 lines (223 loc) · 10.5 KB
/
mixins.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
from django.conf import settings
from django.db import models, IntegrityError, transaction
from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist
from .metaclasses import LisanModelMeta
from .utils import get_translation_service
class LisanModelMixin(models.Model, metaclass=LisanModelMeta):
"""
A Django model mixin that provides multilingual support for models.
This mixin allows models to have translations in different languages by
linking to a corresponding Lisan model. It provides methods to get and
set translations for different fields based on the language code.
"""
_current_language = 'en'
class Meta:
abstract = True
def get_lisan(self, language_code=None):
"""
Retrieve the Lisan (translation) instance for the specified language.
Args:
language_code (str): The language code to retrieve the translation
for. Defaults to the current language.
Returns:
Model or None: The Lisan model instance for the specified language,
or None if not found.
"""
language_code = language_code or self._current_language
try:
return self.lisans.filter(language_code=language_code).first()
except ObjectDoesNotExist:
return None
except Exception as e:
# Log or handle unexpected exceptions if needed
raise e
def set_lisan(self, language_code, **lisan_fields):
"""
Set or update the Lisan (translation) fields for a specified language.
Args:
language_code (str): The language code for the translation.
lisan_fields (dict): Key-value pairs of field names and their
corresponding values.
Returns:
Model: The updated or created Lisan model instance.
Raises:
ValueError: If no language code is provided.
FieldDoesNotExist: If a specified field does not exist in the
translation model.
IntegrityError: If a database integrity issue occurs.
"""
if not language_code:
raise ValueError("Language code must be provided")
try:
with transaction.atomic():
lisan = self.lisans.filter(
language_code=language_code
).first()
if not lisan:
# Check if all lisan_fields exist in the model
for field_name in lisan_fields.keys():
if not hasattr(self.Lisan, field_name):
raise FieldDoesNotExist(
f"Field '{field_name}' does not exist in the "
"translation model."
)
primary_key_field = getattr(self.Lisan._meta, 'pk', None)
if primary_key_field and primary_key_field.name != 'id':
primary_key_value = lisan_fields.pop(
primary_key_field.name, None)
else:
primary_key_value = None
# Explicitly set the foreign key to self (the instance)
# TODO: this is not capturing the UUID for the id
lisan = self.Lisan(
**lisan_fields,
language_code=language_code,
**{self._meta.model_name: self}
)
if primary_key_value is not None:
setattr(
lisan,
primary_key_field.name,
primary_key_value
)
lisan.save()
self.lisans.add(lisan)
else:
for field, value in lisan_fields.items():
if hasattr(lisan, field):
setattr(lisan, field, value)
else:
raise FieldDoesNotExist(
f"Field '{field}' does not exist in the "
"translation model."
)
lisan.save()
return lisan
except IntegrityError:
raise IntegrityError(
"Failed to set translation due to database integrity issues."
)
except FieldDoesNotExist as e:
raise e
except Exception as e:
raise e
def set_bulk_lisans(self, translations):
"""
Set or update translations for multiple languages in bulk.
Args:
translations (dict): A dictionary where each key is a language
code (str) and the corresponding value is a
dictionary of fields (dict) to be set or
updated for that language.
Example:
{
'en': {
'field1': 'value1',
'field2': 'value2'
},
'fr': {
'field1': 'valeur1',
'field2': 'valeur2'
}
}
Behavior:
This method updates the Lisan (translation) model for each
specified language in bulk. For each language code in the
`translations` dictionary, the corresponding fields
and their values are passed to the `set_lisan` method for
that language.
Transaction:
The operation is wrapped in a database transaction to ensure
that either all translations are successfully updated or none
of them are (atomic operation).
Raises:
Exception: Any exceptions encountered during the transaction
will cause the entire operation to roll back, ensuring data
consistency.
"""
with transaction.atomic():
for language_code, fields in translations.items():
self.set_lisan(language_code, **fields)
def get_lisan_field(
self, field_name,
language_code=None,
fallback_languages=None,
auto_translate=False):
"""
Retrieve a specific field's value from the Lisan (translation) model.
Args:
field_name (str): The name of the field to retrieve.
language_code (str, optional): The language code to retrieve the
field for. Defaults to the current
language if not provided.
fallback_languages (list, optional): A list of fallback language
codes to try if the field is
not found in the specified
language. Defaults to a list
of fallback languages set in
the configuration or ['en'].
auto_translate (bool, optional): Whether to automatically translate
the field value if it is not found
in the specified language.
Defaults to False.
Returns:
Any: The value of the specified field from the Lisan translation
model. If the field is not found in any of the provided
languages, it returns the default field value from the main
model. If `auto_translate` is True, it attempts to translate
the value to the requested language if not found.
Raises:
AttributeError: If the field does not exist in either the model or
its translations.
"""
language_code = language_code or self._current_language
fallback_languages = fallback_languages or getattr(
settings, 'LISAN_FALLBACK_LANGUAGES', ['en']
)
# Try to get the field from available translations
for lang in [language_code] + fallback_languages:
lisan = self.get_lisan(lang)
if lisan and hasattr(lisan, field_name):
return getattr(lisan, field_name)
# If auto-translation is enabled, use the translation service
if auto_translate:
original_text = getattr(self, field_name)
translation_service = get_translation_service()
return translation_service.translate(
original_text, target_language=language_code)
# Fallback to default field
return getattr(self, field_name)
def set_current_language(self, language_code):
"""
Set the current language for the model instance.
Args:
language_code (str): The language code to set as the current
language.
Raises:
ValueError: If no language code is provided.
"""
if not language_code:
raise ValueError("Language code must be provided")
self._current_language = language_code
def is_field_translatable(self, field_name):
"""
Determine if the given field is translatable for this instance.
Args:
field_name (str): The name of the field to check for
translatability.
Returns:
bool: True if the field is translatable (i.e., exists in
`lisan_fields`), False otherwise.
Behavior:
This method checks if the specified field is considered
translatable by verifying its presence in the `lisan_fields`
attribute. The `lisan_fields` attribute is expected to
contain a list or set of field names that are marked for
translation.
Example:
>>> instance.is_field_translatable('title')
True
>>> instance.is_field_translatable('non_translatable_field')
False
"""
return field_name in self.lisan_fields