TranslatableBaseInlineFormSet   A
last analyzed

Complexity

Total Complexity 2

Size/Duplication

Total Lines 14
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 2
dl 0
loc 14
c 0
b 0
f 0
rs 10

2 Methods

Rating   Name   Duplication   Size   Complexity  
A _construct_form() 0 4 1
A save_new() 0 3 1
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