123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- """
- Helper functions for creating Form classes from Django models
- and database field objects.
- """
- from django.utils.translation import gettext
- from util import ValidationError
- from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList
- from fields import Field, ChoiceField
- from widgets import Select, SelectMultiple, MultipleHiddenInput
- __all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
- '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``.
- Assumes ``form`` has a field for every non-AutoField database field in
- ``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
- setattr(instance, f.name, cleaned_data[f.name])
- if commit:
- instance.save()
- for f in opts.many_to_many:
- if fields and f.name not in fields:
- continue
- if f.name in cleaned_data:
- setattr(instance, f.attname, cleaned_data[f.name])
- # GOTCHA: If many-to-many data is given and commit=False, the many-to-many
- # data will be lost. This happens because a many-to-many options cannot be
- # set on an object until after it's saved. Maybe we should raise an
- # exception in that case.
- 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 = SortedDictFromList(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 = SortedDictFromList(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 = SortedDictFromList([(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, self.empty_label, self.cache_choices = queryset, empty_label, 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(), str(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.
- def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
- required=True, widget=Select, label=None, initial=None, help_text=None):
- self.queryset = queryset
- 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)
- self.widget.choices = self.choices
- 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 ('', None):
- return None
- try:
- value = self.queryset.model._default_manager.get(pk=value)
- except self.queryset.model.DoesNotExist:
- raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.'))
- return value
- class ModelMultipleChoiceField(ModelChoiceField):
- "A MultipleChoiceField whose choices are a model QuerySet."
- hidden_widget = MultipleHiddenInput
- def __init__(self, queryset, cache_choices=False, required=True,
- widget=SelectMultiple, label=None, initial=None, help_text=None):
- super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices,
- required, widget, label, initial, help_text)
- def clean(self, value):
- if self.required and not value:
- raise ValidationError(gettext(u'This field is required.'))
- elif not self.required and not value:
- return []
- if not isinstance(value, (list, tuple)):
- raise ValidationError(gettext(u'Enter a list of values.'))
- final_values = []
- for val in value:
- try:
- obj = self.queryset.model._default_manager.get(pk=val)
- except self.queryset.model.DoesNotExist:
- raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val)
- else:
- final_values.append(obj)
- return final_values
|