1
|
|
|
from django import forms |
2
|
|
|
import django |
3
|
|
|
from django.core.exceptions import NON_FIELD_ERRORS, ObjectDoesNotExist, ValidationError |
4
|
|
|
from django.forms.forms import BoundField |
5
|
|
|
from django.forms.models import ModelFormMetaclass, BaseInlineFormSet |
6
|
|
|
from django.utils.functional import cached_property |
7
|
|
|
from django.utils.translation import get_language |
8
|
|
|
from django.utils import six |
9
|
|
|
from parler.models import TranslationDoesNotExist |
10
|
|
|
from parler.utils import compat |
11
|
|
|
|
12
|
|
|
|
13
|
|
|
__all__ = ( |
14
|
|
|
'TranslatableModelForm', |
15
|
|
|
'TranslatedField', |
16
|
|
|
'BaseTranslatableModelForm', |
17
|
|
|
#'TranslatableModelFormMetaclass', |
18
|
|
|
) |
19
|
|
|
|
20
|
|
|
|
21
|
|
|
class TranslatedField(object): |
22
|
|
|
""" |
23
|
|
|
A wrapper for a translated form field. |
24
|
|
|
|
25
|
|
|
This wrapper can be used to declare translated fields on the form, e.g. |
26
|
|
|
|
27
|
|
|
.. code-block:: python |
28
|
|
|
|
29
|
|
|
class MyForm(TranslatableModelForm): |
30
|
|
|
title = TranslatedField() |
31
|
|
|
slug = TranslatedField() |
32
|
|
|
|
33
|
|
|
description = TranslatedField(form_class=forms.CharField, widget=TinyMCE) |
34
|
|
|
""" |
35
|
|
|
|
36
|
|
|
def __init__(self, **kwargs): |
37
|
|
|
# The metaclass performs the magic replacement with the actual formfield. |
38
|
|
|
self.kwargs = kwargs |
39
|
|
|
|
40
|
|
|
|
41
|
|
|
class BaseTranslatableModelForm(forms.BaseModelForm): |
42
|
|
|
""" |
43
|
|
|
The base methods added to :class:`TranslatableModelForm` to fetch and store translated fields. |
44
|
|
|
""" |
45
|
|
|
language_code = None # Set by TranslatableAdmin.get_form() on the constructed subclass. |
46
|
|
|
|
47
|
|
|
def __init__(self, *args, **kwargs): |
48
|
|
|
current_language = kwargs.pop('_current_language', None) # Used for TranslatableViewMixin |
49
|
|
|
super(BaseTranslatableModelForm, self).__init__(*args, **kwargs) |
50
|
|
|
|
51
|
|
|
# Load the initial values for the translated fields |
52
|
|
|
instance = kwargs.get('instance', None) |
53
|
|
|
if instance: |
54
|
|
|
for meta in instance._parler_meta: |
55
|
|
|
try: |
56
|
|
|
# By not auto creating a model, any template code that reads the fields |
57
|
|
|
# will continue to see one of the other translations. |
58
|
|
|
# This also causes admin inlines to show the fallback title in __unicode__. |
59
|
|
|
translation = instance._get_translated_model(meta=meta) |
60
|
|
|
except TranslationDoesNotExist: |
61
|
|
|
pass |
62
|
|
|
else: |
63
|
|
|
for field in meta.get_translated_fields(): |
64
|
|
|
try: |
65
|
|
|
self.initial.setdefault(field, getattr(translation, field)) |
66
|
|
|
except ObjectDoesNotExist: |
67
|
|
|
# This occurs when a ForeignKey field is part of the translation, |
68
|
|
|
# but it's value is still not yet, and the field has null=False. |
69
|
|
|
pass |
70
|
|
|
|
71
|
|
|
# Typically already set by admin |
72
|
|
|
if self.language_code is None: |
73
|
|
|
if instance: |
74
|
|
|
self.language_code = instance.get_current_language() |
75
|
|
|
return |
76
|
|
|
else: |
77
|
|
|
self.language_code = current_language or get_language() |
78
|
|
|
|
79
|
|
|
def _get_translation_validation_exclusions(self, translation): |
80
|
|
|
exclude = ['master'] |
81
|
|
|
|
82
|
|
|
# This is the same logic as Django's _get_validation_exclusions(), |
83
|
|
|
# only using the translation model instead of the master instance. |
84
|
|
|
for field_name in translation.get_translated_fields(): |
85
|
|
|
if field_name not in self.fields: |
86
|
|
|
# Exclude fields that aren't on the form. |
87
|
|
|
exclude.append(field_name) |
88
|
|
|
elif self._meta.fields and field_name not in self._meta.fields: |
89
|
|
|
# Field might be added manually at the form, |
90
|
|
|
# but wasn't part of the ModelForm's meta. |
91
|
|
|
exclude.append(field_name) |
92
|
|
|
elif self._meta.exclude and field_name in self._meta.exclude: |
93
|
|
|
# Same for exclude. |
94
|
|
|
exclude.append(field_name) |
95
|
|
|
elif field_name in self._errors.keys(): |
96
|
|
|
# No need to validate fields that already failed. |
97
|
|
|
exclude.append(field_name) |
98
|
|
|
else: |
99
|
|
|
# Exclude fields that are not required in the form, while the model requires them. |
100
|
|
|
# See _get_validation_exclusions() for the detailed bits of this logic. |
101
|
|
|
form_field = self.fields[field_name] |
102
|
|
|
model_field = translation._meta.get_field(field_name) |
103
|
|
|
field_value = self.cleaned_data.get(field_name) |
104
|
|
|
if not model_field.blank and not form_field.required and field_value in form_field.empty_values: |
105
|
|
|
exclude.append(field_name) |
106
|
|
|
|
107
|
|
|
return exclude |
108
|
|
|
|
109
|
|
|
def _post_clean(self): |
110
|
|
|
# Copy the translated fields into the model |
111
|
|
|
# Make sure the language code is set as early as possible (so it's active during most clean() methods) |
112
|
|
|
self.instance.set_current_language(self.language_code) |
113
|
|
|
self.save_translated_fields() |
114
|
|
|
|
115
|
|
|
# Perform the regular clean checks, this also updates self.instance |
116
|
|
|
super(BaseTranslatableModelForm, self)._post_clean() |
117
|
|
|
|
118
|
|
|
def save_translated_fields(self): |
119
|
|
|
""" |
120
|
|
|
Save all translated fields. |
121
|
|
|
""" |
122
|
|
|
fields = {} |
123
|
|
|
|
124
|
|
|
# Collect all translated fields {'name': 'value'} |
125
|
|
|
for field in self._translated_fields: |
126
|
|
|
try: |
127
|
|
|
value = self.cleaned_data[field] |
128
|
|
|
except KeyError: # Field has a ValidationError |
129
|
|
|
continue |
130
|
|
|
fields[field] = value |
131
|
|
|
|
132
|
|
|
# Set the field values on their relevant models |
133
|
|
|
translations = self.instance._set_translated_fields(**fields) |
134
|
|
|
|
135
|
|
|
# Perform full clean on models |
136
|
|
|
non_translated_fields = set(('id', 'master_id', 'language_code')) |
137
|
|
|
for translation in translations: |
138
|
|
|
self._post_clean_translation(translation) |
139
|
|
|
|
140
|
|
|
# Assign translated fields to the model (using the TranslatedAttribute descriptor) |
141
|
|
|
for field in translation._get_field_names(): |
142
|
|
|
if field in non_translated_fields: |
143
|
|
|
continue |
144
|
|
|
setattr(self.instance, field, getattr(translation, field)) |
145
|
|
|
|
146
|
|
|
if django.VERSION >= (1, 6): |
147
|
|
|
|
148
|
|
|
def _post_clean_translation(self, translation): |
149
|
|
|
exclude = self._get_translation_validation_exclusions(translation) |
150
|
|
|
try: |
151
|
|
|
translation.full_clean( |
152
|
|
|
exclude=exclude, validate_unique=False) |
153
|
|
|
except ValidationError as e: |
154
|
|
|
self._update_errors(e) |
155
|
|
|
|
156
|
|
|
# Validate uniqueness if needed. |
157
|
|
|
if self._validate_unique: |
158
|
|
|
try: |
159
|
|
|
translation.validate_unique() |
160
|
|
|
except ValidationError as e: |
161
|
|
|
self._update_errors(e) |
162
|
|
|
else: |
163
|
|
|
|
164
|
|
|
def _post_clean_translation(self, translation): |
165
|
|
|
exclude = self._get_translation_validation_exclusions(translation) |
166
|
|
|
# Clean the model instance's fields. |
167
|
|
|
try: |
168
|
|
|
translation.clean_fields(exclude=exclude) |
169
|
|
|
except ValidationError as e: |
170
|
|
|
self._update_errors(e.message_dict) |
171
|
|
|
|
172
|
|
|
# Call the model instance's clean method. |
173
|
|
|
try: |
174
|
|
|
translation.clean() |
175
|
|
|
except ValidationError as e: |
176
|
|
|
self._update_errors({NON_FIELD_ERRORS: e.messages}) |
177
|
|
|
|
178
|
|
|
# Validate uniqueness if needed. |
179
|
|
|
if self._validate_unique: |
180
|
|
|
try: |
181
|
|
|
translation.validate_unique() |
182
|
|
|
except ValidationError as e: |
183
|
|
|
self._update_errors(e.message_dict) |
184
|
|
|
|
185
|
|
|
@cached_property |
186
|
|
|
def _translated_fields(self): |
187
|
|
|
field_names = self._meta.model._parler_meta.get_all_fields() |
188
|
|
|
return [f_name for f_name in field_names if f_name in self.fields] |
189
|
|
|
|
190
|
|
|
def __getitem__(self, name): |
191
|
|
|
""" |
192
|
|
|
Return a :class:`TranslatableBoundField` for translated models. |
193
|
|
|
This extends the default ``form[field]`` interface that produces the BoundField for HTML templates. |
194
|
|
|
""" |
195
|
|
|
boundfield = super(BaseTranslatableModelForm, self).__getitem__(name) |
196
|
|
|
if name in self._translated_fields: |
197
|
|
|
# Oh the wonders of Python :) |
198
|
|
|
boundfield.__class__ = _upgrade_boundfield_class(boundfield.__class__) |
199
|
|
|
return boundfield |
200
|
|
|
|
201
|
|
|
|
202
|
|
|
UPGRADED_CLASSES = {} |
203
|
|
|
|
204
|
|
|
|
205
|
|
|
def _upgrade_boundfield_class(cls): |
206
|
|
|
if cls is BoundField: |
207
|
|
|
return TranslatableBoundField |
208
|
|
|
elif issubclass(cls, TranslatableBoundField): |
209
|
|
|
return cls |
210
|
|
|
|
211
|
|
|
# When some other package also performs this same trick, |
212
|
|
|
# combine both classes on the fly. Avoid having to do that each time. |
213
|
|
|
# This is needed for django-slug-preview |
214
|
|
|
try: |
215
|
|
|
return UPGRADED_CLASSES[cls] |
216
|
|
|
except KeyError: |
217
|
|
|
# Create once |
218
|
|
|
new_cls = type('Translatable{0}'.format(cls.__name__), (cls, TranslatableBoundField), {}) |
219
|
|
|
UPGRADED_CLASSES[cls] = new_cls |
220
|
|
|
return new_cls |
221
|
|
|
|
222
|
|
|
|
223
|
|
|
class TranslatableBoundField(BoundField): |
224
|
|
|
""" |
225
|
|
|
Decorating the regular BoundField to distinguish translatable fields in the admin. |
226
|
|
|
""" |
227
|
|
|
#: A tagging attribute, making it easy for templates to identify these fields |
228
|
|
|
is_translatable = True |
229
|
|
|
|
230
|
|
|
def label_tag(self, contents=None, attrs=None, *args, **kwargs): # extra args differ per Django version |
231
|
|
|
if attrs is None: |
232
|
|
|
attrs = {} |
233
|
|
|
|
234
|
|
|
attrs['class'] = (attrs.get('class', '') + " translatable-field").strip() |
235
|
|
|
return super(TranslatableBoundField, self).label_tag(contents, attrs, *args, **kwargs) |
236
|
|
|
|
237
|
|
|
# The as_widget() won't be overwritten to add a 'class' attr, |
238
|
|
|
# because it will overwrite what AdminTextInputWidget and fields have as default. |
239
|
|
|
|
240
|
|
|
|
241
|
|
|
class TranslatableModelFormMetaclass(ModelFormMetaclass): |
242
|
|
|
""" |
243
|
|
|
Meta class to add translated form fields to the form. |
244
|
|
|
""" |
245
|
|
|
def __new__(mcs, name, bases, attrs): |
246
|
|
|
# Before constructing class, fetch attributes from bases list. |
247
|
|
|
form_meta = _get_mro_attribute(bases, '_meta') |
248
|
|
|
form_base_fields = _get_mro_attribute(bases, 'base_fields', {}) # set by previous class level. |
249
|
|
|
|
250
|
|
|
if form_meta: |
251
|
|
|
# Not declaring the base class itself, this is a subclass. |
252
|
|
|
|
253
|
|
|
# Read the model from the 'Meta' attribute. This even works in the admin, |
254
|
|
|
# as `modelform_factory()` includes a 'Meta' attribute. |
255
|
|
|
# The other options can be read from the base classes. |
256
|
|
|
form_new_meta = attrs.get('Meta', form_meta) |
257
|
|
|
form_model = form_new_meta.model if form_new_meta else form_meta.model |
258
|
|
|
|
259
|
|
|
# Detect all placeholders at this class level. |
260
|
|
|
placeholder_fields = [ |
261
|
|
|
f_name for f_name, attr_value in six.iteritems(attrs) if isinstance(attr_value, TranslatedField) |
262
|
|
|
] |
263
|
|
|
|
264
|
|
|
# Include the translated fields as attributes, pretend that these exist on the form. |
265
|
|
|
# This also works when assigning `form = TranslatableModelForm` in the admin, |
266
|
|
|
# since the admin always uses modelform_factory() on the form class, and therefore triggering this metaclass. |
267
|
|
|
if form_model: |
268
|
|
|
for translations_model in form_model._parler_meta.get_all_models(): |
269
|
|
|
fields = getattr(form_new_meta, 'fields', form_meta.fields) |
270
|
|
|
exclude = getattr(form_new_meta, 'exclude', form_meta.exclude) or () |
271
|
|
|
widgets = getattr(form_new_meta, 'widgets', form_meta.widgets) or () |
272
|
|
|
formfield_callback = attrs.get('formfield_callback', None) |
273
|
|
|
|
274
|
|
|
if fields == '__all__': |
275
|
|
|
fields = None |
276
|
|
|
|
277
|
|
|
for f_name in translations_model.get_translated_fields(): |
278
|
|
|
# Add translated field if not already added, and respect exclude options. |
279
|
|
|
if f_name in placeholder_fields: |
280
|
|
|
# The TranslatedField placeholder can be replaced directly with actual field, so do that. |
281
|
|
|
attrs[f_name] = _get_model_form_field(translations_model, f_name, formfield_callback=formfield_callback, **attrs[f_name].kwargs) |
282
|
|
|
|
283
|
|
|
# The next code holds the same logic as fields_for_model() |
284
|
|
|
# The f.editable check happens in _get_model_form_field() |
285
|
|
|
elif f_name not in form_base_fields \ |
286
|
|
|
and (fields is None or f_name in fields) \ |
287
|
|
|
and f_name not in exclude \ |
288
|
|
|
and not f_name in attrs: |
289
|
|
|
# Get declared widget kwargs |
290
|
|
|
if f_name in widgets: |
291
|
|
|
# Not combined with declared fields (e.g. the TranslatedField placeholder) |
292
|
|
|
kwargs = {'widget': widgets[f_name]} |
293
|
|
|
else: |
294
|
|
|
kwargs = {} |
295
|
|
|
|
296
|
|
|
# See if this formfield was previously defined using a TranslatedField placeholder. |
297
|
|
|
placeholder = _get_mro_attribute(bases, f_name) |
298
|
|
|
if placeholder and isinstance(placeholder, TranslatedField): |
299
|
|
|
kwargs.update(placeholder.kwargs) |
300
|
|
|
|
301
|
|
|
# Add the form field as attribute to the class. |
302
|
|
|
formfield = _get_model_form_field(translations_model, f_name, formfield_callback=formfield_callback, **kwargs) |
303
|
|
|
if formfield is not None: |
304
|
|
|
attrs[f_name] = formfield |
305
|
|
|
|
306
|
|
|
# Call the super class with updated `attrs` dict. |
307
|
|
|
return super(TranslatableModelFormMetaclass, mcs).__new__(mcs, name, bases, attrs) |
308
|
|
|
|
309
|
|
|
|
310
|
|
|
def _get_mro_attribute(bases, name, default=None): |
311
|
|
|
for base in bases: |
312
|
|
|
try: |
313
|
|
|
return getattr(base, name) |
314
|
|
|
except AttributeError: |
315
|
|
|
continue |
316
|
|
|
return default |
317
|
|
|
|
318
|
|
|
|
319
|
|
|
def _get_model_form_field(model, name, formfield_callback=None, **kwargs): |
320
|
|
|
""" |
321
|
|
|
Utility to create the formfield from a model field. |
322
|
|
|
When a field is not editable, a ``None`` will be returned. |
323
|
|
|
""" |
324
|
|
|
field = model._meta.get_field(name) |
325
|
|
|
if not field.editable: # see fields_for_model() logic in Django. |
326
|
|
|
return None |
327
|
|
|
|
328
|
|
|
# Apply admin formfield_overrides |
329
|
|
|
if formfield_callback is None: |
330
|
|
|
formfield = field.formfield(**kwargs) |
331
|
|
|
elif not callable(formfield_callback): |
332
|
|
|
raise TypeError('formfield_callback must be a function or callable') |
333
|
|
|
else: |
334
|
|
|
formfield = formfield_callback(field, **kwargs) |
335
|
|
|
|
336
|
|
|
return formfield |
337
|
|
|
|
338
|
|
|
|
339
|
|
|
if django.VERSION < (1, 5): |
340
|
|
|
# Django 1.4 doesn't recognize the use of with_metaclass. |
341
|
|
|
# This breaks the form initialization in modelform_factory() |
342
|
|
|
class TranslatableModelForm(BaseTranslatableModelForm, forms.ModelForm): |
343
|
|
|
__metaclass__ = TranslatableModelFormMetaclass |
344
|
|
|
else: |
345
|
|
|
class TranslatableModelForm(compat.with_metaclass(TranslatableModelFormMetaclass, BaseTranslatableModelForm, forms.ModelForm)): |
346
|
|
|
""" |
347
|
|
|
The model form to use for translated models. |
348
|
|
|
""" |
349
|
|
|
|
350
|
|
|
# six.with_metaclass does not handle more than 2 parent classes for django < 1.6 |
351
|
|
|
# but we need all of them in django 1.7 to pass check admin.E016: |
352
|
|
|
# "The value of 'form' must inherit from 'BaseModelForm'" |
353
|
|
|
# so we use our copied version in parler.utils.compat |
354
|
|
|
# |
355
|
|
|
# Also, the class must inherit from ModelForm, |
356
|
|
|
# or the ModelFormMetaclass will skip initialization. |
357
|
|
|
# It only adds the _meta from anything that extends ModelForm. |
358
|
|
|
|
359
|
|
|
|
360
|
|
|
class TranslatableBaseInlineFormSet(BaseInlineFormSet): |
361
|
|
|
""" |
362
|
|
|
The formset base for creating inlines with translatable models. |
363
|
|
|
""" |
364
|
|
|
language_code = None |
365
|
|
|
|
366
|
|
|
def _construct_form(self, i, **kwargs): |
367
|
|
|
form = super(TranslatableBaseInlineFormSet, self)._construct_form(i, **kwargs) |
368
|
|
|
form.language_code = self.language_code # Pass the language code for new objects! |
369
|
|
|
return form |
370
|
|
|
|
371
|
|
|
def save_new(self, form, commit=True): |
372
|
|
|
obj = super(TranslatableBaseInlineFormSet, self).save_new(form, commit) |
373
|
|
|
return obj |
374
|
|
|
|
375
|
|
|
|
376
|
|
|
# Backwards compatibility |
377
|
|
|
TranslatableModelFormMixin = BaseTranslatableModelForm |
378
|
|
|
|