summaryrefslogtreecommitdiff
path: root/django/newforms/models.py
blob: 73d4ba66a079067dc3a70ec3fe486921289db408 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
"""
Helper functions for creating Form classes from Django models
and database field objects.
"""

from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
from django.utils.datastructures import SortedDict

from util import ValidationError
from forms import BaseForm
from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME
from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput

__all__ = (
    'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
    'formset_for_model', 'formset_for_queryset', 'inline_formset',
    'ModelChoiceField', 'ModelMultipleChoiceField',
)

def save_instance(form, instance, fields=None, fail_message='saved',
                  commit=True):
    """
    Saves bound Form ``form``'s cleaned_data into model instance ``instance``.

    If commit=True, then the changes to ``instance`` will be saved to the
    database. Returns ``instance``.
    """
    from django.db import models
    opts = instance.__class__._meta
    if form.errors:
        raise ValueError("The %s could not be %s because the data didn't"
                         " validate." % (opts.object_name, fail_message))
    cleaned_data = form.cleaned_data
    for f in opts.fields:
        if not f.editable or isinstance(f, models.AutoField) \
                or not f.name in cleaned_data:
            continue
        if fields and f.name not in fields:
            continue
        f.save_form_data(instance, cleaned_data[f.name])
    # Wrap up the saving of m2m data as a function.
    def save_m2m():
        opts = instance.__class__._meta
        cleaned_data = form.cleaned_data
        for f in opts.many_to_many:
            if fields and f.name not in fields:
                continue
            if f.name in cleaned_data:
                f.save_form_data(instance, cleaned_data[f.name])
    if commit:
        # If we are committing, save the instance and the m2m data immediately.
        instance.save()
        save_m2m()
    else:
        # We're not committing. Add a method to the form to allow deferred
        # saving of m2m data.
        form.save_m2m = save_m2m
    return instance

def make_model_save(model, fields, fail_message):
    """Returns the save() method for a Form."""
    def save(self, commit=True):
        return save_instance(self, model(), fields, fail_message, commit)
    return save

def make_instance_save(instance, fields, fail_message):
    """Returns the save() method for a Form."""
    def save(self, commit=True):
        return save_instance(self, instance, fields, fail_message, commit)
    return save

def form_for_model(model, form=BaseForm, fields=None,
                   formfield_callback=lambda f: f.formfield()):
    """
    Returns a Form class for the given Django model class.

    Provide ``form`` if you want to use a custom BaseForm subclass.

    Provide ``formfield_callback`` if you want to define different logic for
    determining the formfield for a given database field. It's a callable that
    takes a database Field instance and returns a form Field instance.
    """
    opts = model._meta
    field_list = []
    for f in opts.fields + opts.many_to_many:
        if not f.editable:
            continue
        if fields and not f.name in fields:
            continue
        formfield = formfield_callback(f)
        if formfield:
            field_list.append((f.name, formfield))
    base_fields = SortedDict(field_list)
    return type(opts.object_name + 'Form', (form,),
        {'base_fields': base_fields, '_model': model,
         'save': make_model_save(model, fields, 'created')})

def form_for_instance(instance, form=BaseForm, fields=None,
                      formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
    """
    Returns a Form class for the given Django model instance.

    Provide ``form`` if you want to use a custom BaseForm subclass.

    Provide ``formfield_callback`` if you want to define different logic for
    determining the formfield for a given database field. It's a callable that
    takes a database Field instance, plus **kwargs, and returns a form Field
    instance with the given kwargs (i.e. 'initial').
    """
    model = instance.__class__
    opts = model._meta
    field_list = []
    for f in opts.fields + opts.many_to_many:
        if not f.editable:
            continue
        if fields and not f.name in fields:
            continue
        current_value = f.value_from_object(instance)
        formfield = formfield_callback(f, initial=current_value)
        if formfield:
            field_list.append((f.name, formfield))
    base_fields = SortedDict(field_list)
    return type(opts.object_name + 'InstanceForm', (form,),
        {'base_fields': base_fields, '_model': model,
         'save': make_instance_save(instance, fields, 'changed')})

def form_for_fields(field_list):
    """
    Returns a Form class for the given list of Django database field instances.
    """
    fields = SortedDict([(f.name, f.formfield())
                         for f in field_list if f.editable])
    return type('FormForFields', (BaseForm,), {'base_fields': fields})

class QuerySetIterator(object):
    def __init__(self, queryset, empty_label, cache_choices):
        self.queryset = queryset
        self.empty_label = empty_label
        self.cache_choices = cache_choices

    def __iter__(self):
        if self.empty_label is not None:
            yield (u"", self.empty_label)
        for obj in self.queryset:
            yield (obj._get_pk_val(), smart_unicode(obj))
        # Clear the QuerySet cache if required.
        if not self.cache_choices:
            self.queryset._result_cache = None

class ModelChoiceField(ChoiceField):
    """A ChoiceField whose choices are a model QuerySet."""
    # This class is a subclass of ChoiceField for purity, but it doesn't
    # actually use any of ChoiceField's implementation.
    default_error_messages = {
        'invalid_choice': _(u'Select a valid choice. That choice is not one of'
                            u' the available choices.'),
    }

    def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
                 required=True, widget=Select, label=None, initial=None,
                 help_text=None, *args, **kwargs):
        self.empty_label = empty_label
        self.cache_choices = cache_choices
        # Call Field instead of ChoiceField __init__() because we don't need
        # ChoiceField.__init__().
        Field.__init__(self, required, widget, label, initial, help_text,
                       *args, **kwargs)
        self.queryset = queryset

    def _get_queryset(self):
        return self._queryset

    def _set_queryset(self, queryset):
        self._queryset = queryset
        self.widget.choices = self.choices

    queryset = property(_get_queryset, _set_queryset)

    def _get_choices(self):
        # If self._choices is set, then somebody must have manually set
        # the property self.choices. In this case, just return self._choices.
        if hasattr(self, '_choices'):
            return self._choices
        # Otherwise, execute the QuerySet in self.queryset to determine the
        # choices dynamically. Return a fresh QuerySetIterator that has not
        # been consumed. Note that we're instantiating a new QuerySetIterator
        # *each* time _get_choices() is called (and, thus, each time
        # self.choices is accessed) so that we can ensure the QuerySet has not
        # been consumed.
        return QuerySetIterator(self.queryset, self.empty_label,
                                self.cache_choices)

    def _set_choices(self, value):
        # This method is copied from ChoiceField._set_choices(). It's necessary
        # because property() doesn't allow a subclass to overwrite only
        # _get_choices without implementing _set_choices.
        self._choices = self.widget.choices = list(value)

    choices = property(_get_choices, _set_choices)

    def clean(self, value):
        Field.clean(self, value)
        if value in EMPTY_VALUES:
            return None
        try:
            value = self.queryset.get(pk=value)
        except self.queryset.model.DoesNotExist:
            raise ValidationError(self.error_messages['invalid_choice'])
        return value

class ModelMultipleChoiceField(ModelChoiceField):
    """A MultipleChoiceField whose choices are a model QuerySet."""
    hidden_widget = MultipleHiddenInput
    default_error_messages = {
        'list': _(u'Enter a list of values.'),
        'invalid_choice': _(u'Select a valid choice. %s is not one of the'
                            u' available choices.'),
    }

    def __init__(self, queryset, cache_choices=False, required=True,
                 widget=SelectMultiple, label=None, initial=None,
                 help_text=None, *args, **kwargs):
        super(ModelMultipleChoiceField, self).__init__(queryset, None,
            cache_choices, required, widget, label, initial, help_text,
            *args, **kwargs)

    def clean(self, value):
        if self.required and not value:
            raise ValidationError(self.error_messages['required'])
        elif not self.required and not value:
            return []
        if not isinstance(value, (list, tuple)):
            raise ValidationError(self.error_messages['list'])
        final_values = []
        for val in value:
            try:
                obj = self.queryset.get(pk=val)
            except self.queryset.model.DoesNotExist:
                raise ValidationError(self.error_messages['invalid_choice'] % val)
            else:
                final_values.append(obj)
        return final_values

# Model-FormSet integration ###################################################

def initial_data(instance, fields=None):
    """
    Return a dictionary from data in ``instance`` that is suitable for
    use as a ``Form`` constructor's ``initial`` argument.

    Provide ``fields`` to specify the names of specific fields to return.
    All field values in the instance will be returned if ``fields`` is not
    provided.
    """
    # avoid a circular import
    from django.db.models.fields.related import ManyToManyField
    model = instance.__class__
    opts = model._meta
    initial = {}
    for f in opts.fields + opts.many_to_many:
        if not f.editable:
            continue
        if fields and not f.name in fields:
            continue
        if isinstance(f, ManyToManyField):
            # MultipleChoiceWidget needs a list of ints, not object instances.
            initial[f.name] = [obj._get_pk_val() for obj in f.value_from_object(instance)]
        else:
            initial[f.name] = f.value_from_object(instance)
    return initial

class BaseModelFormSet(BaseFormSet):
    """
    A ``FormSet`` for editing a queryset and/or adding new objects to it.
    """
    model = None
    queryset = None

    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None):
        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
        if self.queryset:
            kwargs['initial'] = [initial_data(obj) for obj in self.get_queryset()]
        super(BaseModelFormSet, self).__init__(**kwargs)

    def get_queryset(self):
        return self.queryset._clone()

    def save_new(self, form, commit=True):
        """Saves and returns a new model instance for the given form."""
        return save_instance(form, self.model(), commit=commit)

    def save_instance(self, form, instance, commit=True):
        """Saves and returns an existing model instance for the given form."""
        return save_instance(form, instance, commit=commit)

    def save(self, commit=True):
        """Saves model instances for every form, adding and changing instances
        as necessary, and returns the list of instances.
        """
        return self.save_existing_objects(commit) + self.save_new_objects(commit)

    def save_existing_objects(self, commit=True):
        if not self.queryset:
            return []
        # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
        existing_objects = {}
        for obj in self.get_queryset():
            existing_objects[obj._get_pk_val()] = obj
        saved_instances = []
        for form in self.change_forms:
            obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
            if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
                obj.delete()
            else:
                saved_instances.append(self.save_instance(form, obj, commit=commit))
        return saved_instances

    def save_new_objects(self, commit=True):
        new_objects = []
        for form in self.add_forms:
            if form.is_empty():
                continue
            # If someone has marked an add form for deletion, don't save the
            # object. At some point it would be nice if we didn't display
            # the deletion widget for add forms.
            if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
                continue
            new_objects.append(self.save_new(form, commit=commit))
        return new_objects

    def add_fields(self, form, index):
        """Add a hidden field for the object's primary key."""
        self._pk_field_name = self.model._meta.pk.attname
        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
        super(BaseModelFormSet, self).add_fields(form, index)

def formset_for_queryset(queryset, form=BaseForm, formfield_callback=lambda f: f.formfield(),
                         formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
    """
    Returns a FormSet class for the given QuerySet. This FormSet will contain
    change forms for every instance in the QuerySet as well as the number of
    add forms specified by ``extra``.
    
    Provide ``extra`` to determine the number of add forms to display.
    
    Provide ``deletable`` if you want to allow the formset to delete any
    objects in the given queryset.
    
    Provide ``form`` if you want to use a custom BaseForm subclass.
    
    Provide ``formfield_callback`` if you want to define different logic for
    determining the formfield for a given database field. It's a callable that
    takes a database Field instance and returns a form Field instance.
    
    Provide ``formset`` if you want to use a custom BaseModelFormSet subclass.
    """
    form = form_for_model(queryset.model, form=form, fields=fields, formfield_callback=formfield_callback)
    FormSet = formset_for_form(form, formset, extra, orderable, deletable)
    FormSet.model = queryset.model
    FormSet.queryset = queryset
    return FormSet

def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
                      formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
    """
    Returns a FormSet class for the given Django model class. This FormSet
    will contain change forms for every instance of the given model as well
    as the number of add forms specified by ``extra``.
    
    This is essentially the same as ``formset_for_queryset``, but automatically
    uses the model's default manager to determine the queryset.
    """
    qs = model._default_manager.all()
    return formset_for_queryset(qs, form, formfield_callback, formset, extra, orderable, deletable, fields)

class InlineFormset(BaseModelFormSet):
    """A formset for child objects related to a parent."""
    def __init__(self, instance, data=None, files=None):
        from django.db.models.fields.related import RelatedObject
        self.instance = instance
        # is there a better way to get the object descriptor?
        self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
        super(InlineFormset, self).__init__(data, files, prefix=self.rel_name)

    def get_queryset(self):
        """
        Returns this FormSet's queryset, but restricted to children of 
        self.instance
        """
        kwargs = {self.fk.name: self.instance}
        return self.queryset.filter(**kwargs)

    def save_new(self, form, commit=True):
        kwargs = {self.fk.get_attname(): self.instance._get_pk_val()}
        new_obj = self.model(**kwargs)
        return save_instance(form, new_obj, commit=commit)

def get_foreign_key(parent_model, model, fk_name=None):
    """
    Finds and returns the ForeignKey from model to parent if there is one.
    If fk_name is provided, assume it is the name of the ForeignKey field.
    """
    # avoid circular import
    from django.db.models import ForeignKey
    opts = model._meta
    if fk_name:
        fks_to_parent = [f for f in opts.fields if f.name == fk_name]
        if len(fks_to_parent) == 1:
            fk = fks_to_parent[0]
            if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
                raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
        elif len(fks_to_parent) == 0:
            raise Exception("%s has no field named '%s'" % (model, fk_name))
    else:
        # Try to discover what the ForeignKey from model to parent_model is
        fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
        if len(fks_to_parent) == 1:
            fk = fks_to_parent[0]
        elif len(fks_to_parent) == 0:
            raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
        else:
            raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
    return fk

def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):
    """
    Returns an ``InlineFormset`` for the given kwargs.

    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    to ``parent_model``.
    """
    fk = get_foreign_key(parent_model, model, fk_name=fk_name)
    # let the formset handle object deletion by default
    FormSet = formset_for_model(model, formset=InlineFormset, fields=fields,
                                formfield_callback=formfield_callback,
                                extra=extra, orderable=orderable,
                                deletable=deletable)
    # HACK: remove the ForeignKey to the parent from every form
    # This should be done a line above before we pass 'fields' to formset_for_model
    # an 'omit' argument would be very handy here
    try:
        del FormSet.form_class.base_fields[fk.name]
    except KeyError:
        pass
    FormSet.fk = fk
    return FormSet