diff -r 57b4279d8c4e -r 03e267d67478 app/django/newforms/models.py --- /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