44
55.. module :: hvad.forms
66
7-
87*******************************
98TranslatableModelFormMetaclass
109*******************************
@@ -14,48 +13,169 @@ TranslatableModelFormMetaclass
1413 Metaclass of :class: `TranslatableModelForm `.
1514
1615 .. method :: __new__(cls, name, bases, attrs)
17-
18- The main thing happening in this metaclass is that the declared and base
19- fields on the form are built by calling
20- :func: `django.forms.models.fields_for_model ` using the correct model
21- depending on whether the field is translated or not. This metaclass also
22- enforces the translations accessor and the master foreign key to be
23- excluded.
16+
17+ Uses Django's internal ``fields_for_model `` to get translated fields
18+ for model and fields declarations, then lets Django handle the other
19+ fields. Once it is done, it merges the translated fields, preserving order.
20+
21+ Special handling is done to:
22+
23+ * Prevent ``language_code `` from being used in any way by a field. This is
24+ because the form uses the ``language_code `` key in the ``cleaned_data ``
25+ dictionary.
26+ * Prevent ``master `` from being recognized as a translated field. It is
27+ still a valid field name though.
28+ * Prevent the translations accessor from being used as a field.
2429
2530
2631**********************
2732TranslatableModelForm
2833**********************
2934
30- .. class :: TranslatableModelForm(ModelForm)
35+ .. class :: BaseTranslatableModelForm(BaseModelForm)
36+
37+ The actual class supporting the features and methods, but lacking metaclass
38+ sugar. Inherited by :class: `~TranslatableModelForm ` to attach the metaclass.
39+ Details are documented on that class.
40+
41+ .. class :: TranslatableModelForm(BaseTranslatableModelForm)
42+
43+ Main form for editing :class: `~hvad.models.TranslatableModel ` instances. As with
44+ regular django :class: `~django.forms.Form ` classes, it can be used either
45+ directly or by passing it to :func: `~translatable_modelform_factory `.
46+
47+ As an extension to regular forms, it handles translation and can be bound
48+ to a language. Binding to a language is done by setting :attr: `language `
49+ on the class (not the instance), either by inheriting it manually or
50+ using the factory function. Once bound to a language, the form is in
51+ **enforce ** mode: all manipulations will be done using that language
52+ exclusively.
3153
3254 .. attribute :: __metaclass__
33-
55+
3456 :class: `TranslatableModelFormMetaclass `
3557
58+ .. attribute :: language
59+
60+ The language the form is bound to. This is a class attribute. If present,
61+ the form is in **enforce ** mode and will only deal with the specified
62+ language. See each method for the exact effects.
63+
3664 .. method :: __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=':', empty_permitted=False, instance=None)
3765
38- If this class is initialized with an instance, it updates ``initial `` to
39- also contain the data from the :term: `Translations Model ` if it can be
40- found.
66+ If this class is initialized with an instance, that has a translation
67+ loaded, it updates ``initial `` to also contain the data from the
68+ :term: `Translations Model `.
69+
70+ If the form is not bound to a language, it will use the data from the
71+ instance. If the instance has no translation loaded, an attempt will be
72+ made at loading the current language, and if that fails the fields will
73+ be blank.
74+
75+ If the form is in **enforce ** mode and the instance does not have the
76+ correct translation loaded, then:
77+
78+ * it will attempt to load it from the database.
79+ * if that fails, it will try to use the loaded translation on the instance.
80+ * if that fails (instance is untranslated), it will use default values.
81+
82+ This process results in new translations being pre-populated with data
83+ from another language. Simply pass an instance in that language, or an
84+ untranslated instance if the behavior is not desired.
85+
86+ .. method :: clean(self)
87+
88+ If the form is in **enforce ** mode, namely if it has a
89+ ``language `` property, apply the it to ``cleaned_data ``. As usual, the
90+ special value ``None `` is replaced by current language.
91+
92+ If the form is not bound to a language, this method does nothing. It is
93+ then possible to either use :meth: `save ` in unbound mode or set the
94+ language code manually in ``cleaned_data['language_code'] ``.
95+
96+ .. note :: A missing language is not the same as ``None``. While ``None``
97+ will be replaced by current language and applied to ``cleaned_data ``,
98+ a missing language will not apply any language at all.
99+
100+ .. method :: _post_clean(self)
101+
102+ Loads a translation appropriate to the form mode. It is the very same that
103+ will be loaded by :meth: `save `. Doing it twice is needed because:
104+
105+ * it must be done in ``_post_clean `` so that the correct translation is
106+ available for modifications. For instance, if the view updates some
107+ translated fields in between the call to ``is_valid() `` and ``save() ``,
108+ or if a form defines a custom ``save() ``.
109+ * it must also be done in ``save `` to ensure the language is correctly
110+ enforced when in **enforce ** mode.
111+
112+ This double check has no cost: unless the instance is changed by the view,
113+ the ``save() `` check will see the translation is correct and do nothing.
41114
42115 .. method :: save(self, commit=True)
43-
116+
44117 Saves both the :term: `Shared Model ` and :term: `Translations Model ` and
45- returns a combined model. The :term: `Translations Model ` is either
46- altered if it already exists on the :term: `Shared Model ` for the current
47- language (which is fetched from the ``language_code `` field on the form
48- or the current active language) or newly created.
49-
50- .. note :: Other than in a normal :class:`django.forms.ModelForm`, this
51- method creates two queries instead of one.
118+ returns a combined model.
52119
53- .. method :: _post_clean(self)
120+ The target language is determined as follows:
121+
122+ * If a language is defined in ``cleaned_data ``, that language is used.
123+ * Else, if the instance has a translation loaded, its language is used.
124+ * Else, the current language is used.
125+
126+ Once the language is determined, the following happen:
127+
128+ * If the object does not exist, it is created.
129+ * If the object exists but not in the target language, its shared fields
130+ are updated and a new translation is created.
131+ * If the object exists in the target language, it is updated.
132+
133+ .. note :: The **enforce** mode has no direct impact on this method. Rather,
134+ it affects the behavior of :meth: `clean `, which places relevant
135+ language (or lack thereof) in ``cleaned_data ``.
136+
137+ .. method :: _get_translation(self, instance, language, enforce)
138+
139+ The internal method :meth: `_post_clean ` and :meth: `save ` delegate
140+ translation checks to. Note that ``enforce `` here refers to the presence
141+ of a ``language_code `` in ``cleaned_data `` and not whether the form is
142+ in **enforce ** mode or not.
143+
144+ This method must gurantee that calling it on its result is a no-op.
145+ Namely, in this example the second call must do nothing and return
146+ the translation as is::
147+
148+ translation = self._get_translation(instance, language, enforce)
149+ instance = combine(translation, instance.__class__)
150+ translation = self._get_translation(instance, language, enforce)
151+
152+
153+ .. function :: translatable_modelform_factory(language, model, form=TranslatableModelForm, **kwargs)
154+
155+ Attaches a language and a model class to the specified form and returns the
156+ resulting class. Additional arguments are any arguments accepted by Django's
157+ :func: `~django.forms.models.modelform_factory `, including ``fields `` and
158+ ``exclude ``.
159+
160+ Having a language attached, the returned form is in **enforce ** mode.
161+
162+ .. function :: translatable_modelformset_factory(language, model, form=TranslatableModelForm, **kwargs)
163+
164+ Creates a formset class, allowing edition a collection of instances of ``model ``,
165+ all of them in the specified ``language ``. Additional arguments are any
166+ argument accepted by Django's :func: `~django.forms.models.modelformset_factory `.
167+
168+ Having a language attached, the returned formset is in **enforce ** mode.
169+
170+ .. function :: translatable_inlineformset_factory(language, parent_model, model, form=TranslatableModelForm, **kwargs)
171+
172+ Creates an inline formset, allowing edition of a collection of instances of
173+ ``model `` attached to an instance of ``parent_model ``, all of those objects
174+ being in the specified ``language ``. Additional arguments are any argument
175+ accepted by Django's :func: `~django.forms.models.inlineformset_factory `.
176+
177+ Having a language attached, the returned formset is in **enforce ** mode.
54178
55- Ensures the correct translation is loaded into **self.instance **.
56- It tries to load the language specified in the form's **language_code **
57- field from the database, and calls
58- :meth: `~hvad.models.TranslatableModel.translate ` if it does not exist yet.
59179
60180**********************
61181BaseTranslationFormSet
0 commit comments