--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/django/newforms/models.py Fri Jul 18 18:22:23 2008 +0000
@@ -0,0 +1,398 @@
+"""
+Helper functions for creating Form classes from Django models
+and database field objects.
+"""
+
+from warnings import warn
+
+from django.utils.translation import ugettext_lazy as _
+from django.utils.encoding import smart_unicode
+from django.utils.datastructures import SortedDict
+from django.core.exceptions import ImproperlyConfigured
+
+from util import ValidationError, ErrorList
+from forms import BaseForm, get_declared_fields
+from fields import Field, ChoiceField, EMPTY_VALUES
+from widgets import Select, SelectMultiple, MultipleHiddenInput
+
+__all__ = (
+ 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
+ '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``.
+
+ 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.
+ """
+ warn("form_for_model is deprecated. Use ModelForm instead.",
+ PendingDeprecationWarning, stacklevel=3)
+ 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').
+ """
+ warn("form_for_instance is deprecated. Use ModelForm instead.",
+ PendingDeprecationWarning, stacklevel=3)
+ 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})
+
+
+# ModelForms #################################################################
+
+def model_to_dict(instance, fields=None, exclude=None):
+ """
+ Returns a dict containing the data in ``instance`` suitable for passing as
+ a Form's ``initial`` keyword argument.
+
+ ``fields`` is an optional list of field names. If provided, only the named
+ fields will be included in the returned dict.
+
+ ``exclude`` is an optional list of field names. If provided, the named
+ fields will be excluded from the returned dict, even if they are listed in
+ the ``fields`` argument.
+ """
+ # avoid a circular import
+ from django.db.models.fields.related import ManyToManyField
+ opts = instance._meta
+ data = {}
+ for f in opts.fields + opts.many_to_many:
+ if not f.editable:
+ continue
+ if fields and not f.name in fields:
+ continue
+ if exclude and f.name in exclude:
+ continue
+ if isinstance(f, ManyToManyField):
+ # If the object doesn't have a primry key yet, just use an empty
+ # list for its m2m fields. Calling f.value_from_object will raise
+ # an exception.
+ if instance.pk is None:
+ data[f.name] = []
+ else:
+ # MultipleChoiceWidget needs a list of pks, not object instances.
+ data[f.name] = [obj.pk for obj in f.value_from_object(instance)]
+ else:
+ data[f.name] = f.value_from_object(instance)
+ return data
+
+def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda f: f.formfield()):
+ """
+ Returns a ``SortedDict`` containing form fields for the given model.
+
+ ``fields`` is an optional list of field names. If provided, only the named
+ fields will be included in the returned fields.
+
+ ``exclude`` is an optional list of field names. If provided, the named
+ fields will be excluded from the returned fields, even if they are listed
+ in the ``fields`` argument.
+ """
+ # TODO: if fields is provided, it would be nice to return fields in that order
+ field_list = []
+ opts = model._meta
+ for f in opts.fields + opts.many_to_many:
+ if not f.editable:
+ continue
+ if fields and not f.name in fields:
+ continue
+ if exclude and f.name in exclude:
+ continue
+ formfield = formfield_callback(f)
+ if formfield:
+ field_list.append((f.name, formfield))
+ return SortedDict(field_list)
+
+class ModelFormOptions(object):
+ def __init__(self, options=None):
+ self.model = getattr(options, 'model', None)
+ self.fields = getattr(options, 'fields', None)
+ self.exclude = getattr(options, 'exclude', None)
+
+
+class ModelFormMetaclass(type):
+ def __new__(cls, name, bases, attrs,
+ formfield_callback=lambda f: f.formfield()):
+ try:
+ parents = [b for b in bases if issubclass(b, ModelForm)]
+ except NameError:
+ # We are defining ModelForm itself.
+ parents = None
+ if not parents:
+ return super(ModelFormMetaclass, cls).__new__(cls, name, bases,
+ attrs)
+
+ new_class = type.__new__(cls, name, bases, attrs)
+ declared_fields = get_declared_fields(bases, attrs, False)
+ opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
+ if opts.model:
+ # If a model is defined, extract form fields from it.
+ fields = fields_for_model(opts.model, opts.fields,
+ opts.exclude, formfield_callback)
+ # Override default model fields with any custom declared ones
+ # (plus, include all the other declared fields).
+ fields.update(declared_fields)
+ else:
+ fields = declared_fields
+ new_class.declared_fields = declared_fields
+ new_class.base_fields = fields
+ return new_class
+
+class BaseModelForm(BaseForm):
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ initial=None, error_class=ErrorList, label_suffix=':',
+ instance=None):
+ opts = self._meta
+ if instance is None:
+ # if we didn't get an instance, instantiate a new one
+ self.instance = opts.model()
+ object_data = {}
+ else:
+ self.instance = instance
+ object_data = model_to_dict(instance, opts.fields, opts.exclude)
+ # if initial was provided, it should override the values from instance
+ if initial is not None:
+ object_data.update(initial)
+ BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix)
+
+ def save(self, commit=True):
+ """
+ Saves this ``form``'s cleaned_data into model instance
+ ``self.instance``.
+
+ If commit=True, then the changes to ``instance`` will be saved to the
+ database. Returns ``instance``.
+ """
+ if self.instance.pk is None:
+ fail_message = 'created'
+ else:
+ fail_message = 'changed'
+ return save_instance(self, self.instance, self._meta.fields, fail_message, commit)
+
+class ModelForm(BaseModelForm):
+ __metaclass__ = ModelFormMetaclass
+
+
+# Fields #####################################################################
+
+class ModelChoiceIterator(object):
+ def __init__(self, field):
+ self.field = field
+ self.queryset = field.queryset
+
+ def __iter__(self):
+ if self.field.empty_label is not None:
+ yield (u"", self.field.empty_label)
+ for obj in self.queryset:
+ yield (obj.pk, self.field.label_from_instance(obj))
+ # Clear the QuerySet cache if required.
+ if not self.field.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)
+
+ # this method will be used to create object labels by the QuerySetIterator.
+ # Override it to customize the label.
+ def label_from_instance(self, obj):
+ """
+ This method is used to convert objects into strings; it's used to
+ generate the labels for the choices presented by this object. Subclasses
+ can override this method to customize the display of the choices.
+ """
+ return smart_unicode(obj)
+
+ 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. This
+ # construct might look complicated but it allows for lazy evaluation of
+ # the queryset.
+ return ModelChoiceIterator(self)
+
+ 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