| 1 | """ |
|---|
| 2 | Helper functions for creating Form classes from Django models |
|---|
| 3 | and database field objects. |
|---|
| 4 | |
|---|
| 5 | Enhanced form_for_instance fills in missing fields of the form |
|---|
| 6 | object's data from the original model instance. See 'Changed from |
|---|
| 7 | stock newforms' in docstrings and comments. Implemented by akaihola |
|---|
| 8 | 2007-02-08. |
|---|
| 9 | """ |
|---|
| 10 | |
|---|
| 11 | try: |
|---|
| 12 | from django import newforms as forms |
|---|
| 13 | except ImportError: |
|---|
| 14 | from django import forms |
|---|
| 15 | |
|---|
| 16 | def 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 | |
|---|
| 27 | def 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 | |
|---|
| 70 | def 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 | |
|---|
| 76 | def 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 | |
|---|
| 125 | def 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 | |
|---|
| 157 | if 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 | |
|---|
| 184 | if 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 |
|---|