""" 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 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): # TODO: no way to specify formfield_callback yet, do we need one, or # should it be a special case for the admin? fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) # If this class is subclassing another Form, add that Form's fields. # Note that we loop over the bases in *reverse*. This is necessary in # order to preserve the correct order of fields. for base in bases[::-1]: if hasattr(base, 'base_fields'): fields = base.base_fields.items() + fields declared_fields = SortedDict(fields) opts = ModelFormOptions(attrs.get('Meta', None)) attrs['_meta'] = opts # Don't allow more than one Meta model defenition in bases. The fields # would be generated correctly, but the save method won't deal with # more than one object. base_models = [] for base in bases: base_opts = getattr(base, '_meta', None) base_model = getattr(base_opts, 'model', None) if base_model is not None: base_models.append(base_model) if len(base_models) > 1: raise ImproperlyConfigured("%s's base classes define more than one model." % name) # If a model is defined, extract form fields from it and add them to base_fields if attrs['_meta'].model is not None: # Don't allow a subclass to define a Meta model if a parent class has. # Technically the right fields would be generated, but the save # method will not deal with more than one model. for base in bases: base_opts = getattr(base, '_meta', None) base_model = getattr(base_opts, 'model', None) if base_model is not None: raise ImproperlyConfigured('%s defines more than one model.' % name) model_fields = fields_for_model(opts.model, opts.fields, opts.exclude) # fields declared in base classes override fields from the model model_fields.update(declared_fields) attrs['base_fields'] = model_fields else: attrs['base_fields'] = declared_fields return type.__new__(cls, name, bases, attrs) class BaseModelForm(BaseForm): def __init__(self, instance, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=':'): self.instance = instance opts = self._meta 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 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.pk, 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