1
|
|
|
from django import forms |
2
|
|
|
import django |
3
|
|
|
from django.core.exceptions import ObjectDoesNotExist |
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 _post_clean(self): |
80
|
|
|
# Copy the translated fields into the model |
81
|
|
|
# Make sure the language code is set as early as possible (so it's active during most clean() methods) |
82
|
|
|
self.instance.set_current_language(self.language_code) |
83
|
|
|
self.save_translated_fields() |
84
|
|
|
|
85
|
|
|
# Perform the regular clean checks, this also updates self.instance |
86
|
|
|
super(BaseTranslatableModelForm, self)._post_clean() |
87
|
|
|
|
88
|
|
|
def save_translated_fields(self): |
89
|
|
|
""" |
90
|
|
|
Save all translated fields. |
91
|
|
|
""" |
92
|
|
|
# Assign translated fields to the model (using the TranslatedAttribute descriptor) |
93
|
|
|
for field in self._translated_fields: |
94
|
|
|
try: |
95
|
|
|
value = self.cleaned_data[field] |
96
|
|
|
except KeyError: # Field has a ValidationError |
97
|
|
|
continue |
98
|
|
|
setattr(self.instance, field, value) |
99
|
|
|
|
100
|
|
|
@cached_property |
101
|
|
|
def _translated_fields(self): |
102
|
|
|
field_names = self._meta.model._parler_meta.get_all_fields() |
103
|
|
|
return [f_name for f_name in field_names if f_name in self.fields] |
104
|
|
|
|
105
|
|
|
def __getitem__(self, name): |
106
|
|
|
""" |
107
|
|
|
Return a :class:`TranslatableBoundField` for translated models. |
108
|
|
|
This extends the default ``form[field]`` interface that produces the BoundField for HTML templates. |
109
|
|
|
""" |
110
|
|
|
boundfield = super(BaseTranslatableModelForm, self).__getitem__(name) |
111
|
|
|
if name in self._translated_fields: |
112
|
|
|
# Oh the wonders of Python :) |
113
|
|
|
boundfield.__class__ = _upgrade_boundfield_class(boundfield.__class__) |
114
|
|
|
return boundfield |
115
|
|
|
|
116
|
|
|
|
117
|
|
|
UPGRADED_CLASSES = {} |
118
|
|
|
|
119
|
|
|
|
120
|
|
|
def _upgrade_boundfield_class(cls): |
121
|
|
|
if cls is BoundField: |
122
|
|
|
return TranslatableBoundField |
123
|
|
|
elif issubclass(cls, TranslatableBoundField): |
124
|
|
|
return cls |
125
|
|
|
|
126
|
|
|
# When some other package also performs this same trick, |
127
|
|
|
# combine both classes on the fly. Avoid having to do that each time. |
128
|
|
|
# This is needed for django-slug-preview |
129
|
|
|
try: |
130
|
|
|
return UPGRADED_CLASSES[cls] |
131
|
|
|
except KeyError: |
132
|
|
|
# Create once |
133
|
|
|
new_cls = type('Translatable{0}'.format(cls.__name__), (cls, TranslatableBoundField), {}) |
134
|
|
|
UPGRADED_CLASSES[cls] = new_cls |
135
|
|
|
return new_cls |
136
|
|
|
|
137
|
|
|
|
138
|
|
|
class TranslatableBoundField(BoundField): |
139
|
|
|
""" |
140
|
|
|
Decorating the regular BoundField to distinguish translatable fields in the admin. |
141
|
|
|
""" |
142
|
|
|
#: A tagging attribute, making it easy for templates to identify these fields |
143
|
|
|
is_translatable = True |
144
|
|
|
|
145
|
|
|
def label_tag(self, contents=None, attrs=None, *args, **kwargs): # extra args differ per Django version |
146
|
|
|
if attrs is None: |
147
|
|
|
attrs = {} |
148
|
|
|
|
149
|
|
|
attrs['class'] = (attrs.get('class', '') + " translatable-field").strip() |
150
|
|
|
return super(TranslatableBoundField, self).label_tag(contents, attrs, *args, **kwargs) |
151
|
|
|
|
152
|
|
|
# The as_widget() won't be overwritten to add a 'class' attr, |
153
|
|
|
# because it will overwrite what AdminTextInputWidget and fields have as default. |
154
|
|
|
|
155
|
|
|
|
156
|
|
|
class TranslatableModelFormMetaclass(ModelFormMetaclass): |
157
|
|
|
""" |
158
|
|
|
Meta class to add translated form fields to the form. |
159
|
|
|
""" |
160
|
|
|
def __new__(mcs, name, bases, attrs): |
161
|
|
|
# Before constructing class, fetch attributes from bases list. |
162
|
|
|
form_meta = _get_mro_attribute(bases, '_meta') |
163
|
|
|
form_base_fields = _get_mro_attribute(bases, 'base_fields', {}) # set by previous class level. |
164
|
|
|
|
165
|
|
|
if form_meta: |
166
|
|
|
# Not declaring the base class itself, this is a subclass. |
167
|
|
|
|
168
|
|
|
# Read the model from the 'Meta' attribute. This even works in the admin, |
169
|
|
|
# as `modelform_factory()` includes a 'Meta' attribute. |
170
|
|
|
# The other options can be read from the base classes. |
171
|
|
|
form_new_meta = attrs.get('Meta', form_meta) |
172
|
|
|
form_model = form_new_meta.model if form_new_meta else form_meta.model |
173
|
|
|
|
174
|
|
|
# Detect all placeholders at this class level. |
175
|
|
|
placeholder_fields = [ |
176
|
|
|
f_name for f_name, attr_value in six.iteritems(attrs) if isinstance(attr_value, TranslatedField) |
177
|
|
|
] |
178
|
|
|
|
179
|
|
|
# Include the translated fields as attributes, pretend that these exist on the form. |
180
|
|
|
# This also works when assigning `form = TranslatableModelForm` in the admin, |
181
|
|
|
# since the admin always uses modelform_factory() on the form class, and therefore triggering this metaclass. |
182
|
|
|
if form_model: |
183
|
|
|
for translations_model in form_model._parler_meta.get_all_models(): |
184
|
|
|
fields = getattr(form_new_meta, 'fields', form_meta.fields) |
185
|
|
|
exclude = getattr(form_new_meta, 'exclude', form_meta.exclude) or () |
186
|
|
|
widgets = getattr(form_new_meta, 'widgets', form_meta.widgets) or () |
187
|
|
|
formfield_callback = attrs.get('formfield_callback', None) |
188
|
|
|
|
189
|
|
|
if fields == '__all__': |
190
|
|
|
fields = None |
191
|
|
|
|
192
|
|
|
for f_name in translations_model.get_translated_fields(): |
193
|
|
|
# Add translated field if not already added, and respect exclude options. |
194
|
|
|
if f_name in placeholder_fields: |
195
|
|
|
# The TranslatedField placeholder can be replaced directly with actual field, so do that. |
196
|
|
|
attrs[f_name] = _get_model_form_field(translations_model, f_name, formfield_callback=formfield_callback, **attrs[f_name].kwargs) |
197
|
|
|
|
198
|
|
|
# The next code holds the same logic as fields_for_model() |
199
|
|
|
# The f.editable check happens in _get_model_form_field() |
200
|
|
|
elif f_name not in form_base_fields \ |
201
|
|
|
and (fields is None or f_name in fields) \ |
202
|
|
|
and f_name not in exclude \ |
203
|
|
|
and not f_name in attrs: |
204
|
|
|
# Get declared widget kwargs |
205
|
|
|
if f_name in widgets: |
206
|
|
|
# Not combined with declared fields (e.g. the TranslatedField placeholder) |
207
|
|
|
kwargs = {'widget': widgets[f_name]} |
208
|
|
|
else: |
209
|
|
|
kwargs = {} |
210
|
|
|
|
211
|
|
|
# See if this formfield was previously defined using a TranslatedField placeholder. |
212
|
|
|
placeholder = _get_mro_attribute(bases, f_name) |
213
|
|
|
if placeholder and isinstance(placeholder, TranslatedField): |
214
|
|
|
kwargs.update(placeholder.kwargs) |
215
|
|
|
|
216
|
|
|
# Add the form field as attribute to the class. |
217
|
|
|
formfield = _get_model_form_field(translations_model, f_name, formfield_callback=formfield_callback, **kwargs) |
218
|
|
|
if formfield is not None: |
219
|
|
|
attrs[f_name] = formfield |
220
|
|
|
|
221
|
|
|
# Call the super class with updated `attrs` dict. |
222
|
|
|
return super(TranslatableModelFormMetaclass, mcs).__new__(mcs, name, bases, attrs) |
223
|
|
|
|
224
|
|
|
|
225
|
|
|
def _get_mro_attribute(bases, name, default=None): |
226
|
|
|
for base in bases: |
227
|
|
|
try: |
228
|
|
|
return getattr(base, name) |
229
|
|
|
except AttributeError: |
230
|
|
|
continue |
231
|
|
|
return default |
232
|
|
|
|
233
|
|
|
|
234
|
|
|
def _get_model_form_field(model, name, formfield_callback=None, **kwargs): |
235
|
|
|
""" |
236
|
|
|
Utility to create the formfield from a model field. |
237
|
|
|
When a field is not editable, a ``None`` will be returned. |
238
|
|
|
""" |
239
|
|
|
field = model._meta.get_field(name) |
240
|
|
|
if not field.editable: # see fields_for_model() logic in Django. |
241
|
|
|
return None |
242
|
|
|
|
243
|
|
|
# Apply admin formfield_overrides |
244
|
|
|
if formfield_callback is None: |
245
|
|
|
formfield = field.formfield(**kwargs) |
246
|
|
|
elif not callable(formfield_callback): |
247
|
|
|
raise TypeError('formfield_callback must be a function or callable') |
248
|
|
|
else: |
249
|
|
|
formfield = formfield_callback(field, **kwargs) |
250
|
|
|
|
251
|
|
|
return formfield |
252
|
|
|
|
253
|
|
|
|
254
|
|
|
if django.VERSION < (1, 5): |
255
|
|
|
# Django 1.4 doesn't recognize the use of with_metaclass. |
256
|
|
|
# This breaks the form initialization in modelform_factory() |
257
|
|
|
class TranslatableModelForm(BaseTranslatableModelForm, forms.ModelForm): |
258
|
|
|
__metaclass__ = TranslatableModelFormMetaclass |
259
|
|
|
else: |
260
|
|
|
class TranslatableModelForm(compat.with_metaclass(TranslatableModelFormMetaclass, BaseTranslatableModelForm, forms.ModelForm)): |
261
|
|
|
""" |
262
|
|
|
The model form to use for translated models. |
263
|
|
|
""" |
264
|
|
|
|
265
|
|
|
# six.with_metaclass does not handle more than 2 parent classes for django < 1.6 |
266
|
|
|
# but we need all of them in django 1.7 to pass check admin.E016: |
267
|
|
|
# "The value of 'form' must inherit from 'BaseModelForm'" |
268
|
|
|
# so we use our copied version in parler.utils.compat |
269
|
|
|
# |
270
|
|
|
# Also, the class must inherit from ModelForm, |
271
|
|
|
# or the ModelFormMetaclass will skip initialization. |
272
|
|
|
# It only adds the _meta from anything that extends ModelForm. |
273
|
|
|
|
274
|
|
|
|
275
|
|
|
class TranslatableBaseInlineFormSet(BaseInlineFormSet): |
276
|
|
|
""" |
277
|
|
|
The formset base for creating inlines with translatable models. |
278
|
|
|
""" |
279
|
|
|
language_code = None |
280
|
|
|
|
281
|
|
|
def _construct_form(self, i, **kwargs): |
282
|
|
|
form = super(TranslatableBaseInlineFormSet, self)._construct_form(i, **kwargs) |
283
|
|
|
form.language_code = self.language_code # Pass the language code for new objects! |
284
|
|
|
return form |
285
|
|
|
|
286
|
|
|
def save_new(self, form, commit=True): |
287
|
|
|
obj = super(TranslatableBaseInlineFormSet, self).save_new(form, commit) |
288
|
|
|
return obj |
289
|
|
|
|
290
|
|
|
|
291
|
|
|
# Backwards compatibility |
292
|
|
|
TranslatableModelFormMixin = BaseTranslatableModelForm |
293
|
|
|
|