root/trunk/newforms_extra/models.py

Revision 274, 8.8 kB (checked in by akaihola, 23 months ago)

[newforms_extra] Fixed Django <1.0 compatibility.

Line 
1"""
2Helper functions for creating Form classes from Django models
3and database field objects.
4
5Enhanced form_for_instance fills in missing fields of the form
6object's data from the original model instance.  See 'Changed from
7stock newforms' in docstrings and comments.  Implemented by akaihola
82007-02-08.
9"""
10
11try:
12    from django import newforms as forms
13except ImportError:
14    from django import forms
15
16def model_save(self, commit=True):
17    """
18    Creates and returns model instance according to self.clean_data.
19
20    This method is created for any form_for_model Form.
21    """
22    if self.errors:
23        raise ValueError("The %s could not be created because the data didn't "
24                         "validate." % self._model._meta.object_name)
25    return save_instance(self, self._model(), commit)
26
27def save_instance(form, instance, commit=True):
28    """
29    Saves bound Form ``form``'s clean_data into model instance ``instance``.
30
31    Assumes ``form`` has a field for every non-AutoField database field in
32    ``instance``. If commit=True, then the changes to ``instance`` will be
33    saved to the database. Returns ``instance``.
34    """
35    from django.db import models
36    opts = instance.__class__._meta
37    if form.errors:
38        raise ValueError("The %s could not be changed because "
39                         "the data didn't validate." % opts.object_name)
40    try:
41        # Django newer than May 17, 2007
42        clean_data = form.cleaned_data
43    except AttributeError:
44        # Django older than May 17, 2007
45        clean_data = form.clean_data
46
47    for f in opts.fields:
48        if isinstance(f, models.AutoField):
49            continue
50
51        # Changed from stock newforms:
52        # only set values for fields actually found in the form
53        # and keep old values for the rest of the fields
54        if f.name in clean_data:
55            value = clean_data[f.name]
56            if isinstance(value, models.Model):
57                value = value._get_pk_val()
58            setattr(instance, f.attname, value)
59
60    if commit:
61        instance.save()
62        for f in opts.many_to_many:
63            setattr(instance, f.attname, clean_data[f.name])
64    # GOTCHA: If many-to-many data is given and commit=False, the many-to-many
65    # data will be lost. This happens because a many-to-many options cannot be
66    # set on an object until after it's saved. Maybe we should raise an
67    # exception in that case.
68    return instance
69
70def make_instance_save(instance):
71    "Returns the save() method for a form_for_instance Form."
72    def save(self, commit=True):
73        return save_instance(self, instance, commit)
74    return save
75
76def form_for_instance(instance, form=forms.BaseForm,
77                      formfield_callback=lambda f, **kwargs:
78                      f.formfield(**kwargs),
79                      include_fields=None, exclude_fields=None):
80    """
81    Returns a Form class for the given Django model instance.
82
83    Provide ``form`` if you want to use a custom BaseForm subclass.
84
85    Provide ``formfield_callback`` if you want to define different logic for
86    determining the formfield for a given database field. It's a callable that
87    takes a database Field instance, plus **kwargs, and returns a form Field
88    instance with the given kwargs (i.e. 'initial').
89
90    Changed from stock newforms:
91    - added parameters ``include_fields`` and ``exclude_fields``:
92      * if ``include_fields`` is specified, only fieldnames in that
93        list are used in the form
94      * if ``exclude_fields`` is specified, fieldnames in that list
95        are not used in the form
96      * both ``include_fields`` and ``exclude_fields`` cannot be
97        specified at the same time
98    """
99    if include_fields is not None and exclude_fields is not None:
100        raise ValueError("form_for_instance can't use both "
101                         "include_fields and exclude_fields at the same time")
102    model = instance.__class__
103    opts = model._meta
104    field_list = []
105    for f in opts.fields + opts.many_to_many:
106
107        # Changed from stock newforms:
108        # Do not use fields which are not in ``include_fields`` (if it is
109        # specified) or which are in ``exclude_fields`` (if it is specified).
110        if (include_fields and f.name not in include_fields) or \
111           (exclude_fields and f.name in exclude_fields):
112            continue
113
114        current_value = f.value_from_object(instance)
115        formfield = formfield_callback(f, initial=current_value)
116        if formfield:
117            field_list.append((f.name, formfield))
118    fields = forms.SortedDictFromList(field_list)
119    return type(opts.object_name + 'InstanceForm', (form,),
120        {'base_fields': fields,
121         '_model': model,
122         'save': make_instance_save(instance)})
123
124
125def get_changed_fields(form, instance, fields=None):
126    """
127    Compare the validated form to the given instance and return a list of
128    fields whose values don't match.  The ``fields`` keyword argument can be
129    used to specify a limited set of fields to compare.  All fields are
130    compared by default.
131    """
132    opts = instance._meta
133    cleaned_data = form.cleaned_data
134    changed_field_names = set()
135    for f in opts.fields:
136        if fields and f.name not in fields:
137            continue
138        if f.name in cleaned_data:
139            if getattr(instance, f.name) != cleaned_data[f.name]:
140                changed_field_names.add(f.name)
141    for f in opts.many_to_many:
142        if fields and f.name not in fields:
143            continue
144        if f.name in cleaned_data:
145            if instance.pk is None: # creating a new object
146                changed_field_names.add(f.name)
147                continue
148            instvalue = set(field._get_pk_val()
149                            for field in getattr(instance, f.name).all())
150            formvalue = set(field._get_pk_val()
151                            for field in cleaned_data[f.name])
152            if instvalue != formvalue:
153                changed_field_names.add(f.name)
154    return changed_field_names
155
156
157if hasattr(forms.models, 'InlineFormset'):
158    def inline_formset(parent_model, model,
159                       form=forms.BaseForm, formset=forms.models.InlineFormset,
160                       fk_name=None, fields=None, extra=3, orderable=False,
161                       deletable=True, formfield_callback=lambda f: f.formfield()):
162        """
163        Returns an ``InlineFormset`` for the given kwargs.
164
165        You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
166        to ``parent_model``.
167
168        This modified version of the ``newforms.models.inline_formset()`` function
169        is a temporary fix while #5759 is not yet fixed.  -- akaihola 2007-10-15
170        """
171        fk = forms.models.get_foreign_key(parent_model, model, fk_name=fk_name)
172        FormSet = forms.formset_for_model(model, formset=formset, fields=fields,
173                                          formfield_callback=formfield_callback,
174                                          extra=extra, orderable=orderable,
175                                          deletable=deletable, form=form)
176        try:
177            del FormSet.form_class.base_fields[fk.name]
178        except KeyError:
179            pass
180        FormSet.fk = fk
181        return FormSet
182
183
184if hasattr(forms, 'ModelForm'):
185
186    # ModelForms have been in Django since r6844.  These classes implement
187    # enhancements to them.
188
189    class SmartModelFormData:
190        """
191        Helper class for SmartModelForm.
192        """
193        def __init__(self, form):
194            self.form = form
195        def __getitem__(self, attr):
196            return self.form._get_field_val(attr)
197
198    class SmartModelForm(forms.ModelForm):
199        """
200        With this model form base class you can access underlying form data in a
201        template:
202
203        {{ myform.field_values.fieldname }}
204
205        This is useful if you need to display some data from the model instance
206        without having a form field for it.
207        """
208        def __init__(self, *args, **kwargs):
209            super(SmartModelForm, self).__init__(*args, **kwargs)
210            self.field_values = SmartModelFormData(self)
211
212        def _get_field_val(self, fieldname):
213            """
214            Return the value for the given field from the POST data for bound forms
215            and initial data for non-bound forms.  This is really just a hack...
216            """
217            if self.is_bound:
218                if fieldname in self:
219                    value = self[fieldname].data
220                else:
221                    value = None
222            else:
223                value = self.initial.get(fieldname, None)
224            if value is not None:
225                from django.db import models
226                field = self._meta.model._meta.get_field(fieldname)
227                if isinstance(field, models.ForeignKey):
228                    value = field.rel.to.objects.get(pk=value)
229            return value
Note: See TracBrowser for help on using the browser.