|
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
|
|
|
|