/litclub/django/newforms/models.py
Python | 398 lines | 370 code | 9 blank | 19 comment | 1 complexity | 901ef20acdd170484fcfe3626a50d34c MD5 | raw file
- """
- 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