app/django/newforms/models.py
changeset 54 03e267d67478
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     1 """
       
     2 Helper functions for creating Form classes from Django models
       
     3 and database field objects.
       
     4 """
       
     5 
       
     6 from warnings import warn
       
     7 
       
     8 from django.utils.translation import ugettext_lazy as _
       
     9 from django.utils.encoding import smart_unicode
       
    10 from django.utils.datastructures import SortedDict
       
    11 from django.core.exceptions import ImproperlyConfigured
       
    12 
       
    13 from util import ValidationError, ErrorList
       
    14 from forms import BaseForm, get_declared_fields
       
    15 from fields import Field, ChoiceField, EMPTY_VALUES
       
    16 from widgets import Select, SelectMultiple, MultipleHiddenInput
       
    17 
       
    18 __all__ = (
       
    19     'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
       
    20     'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
       
    21     'ModelChoiceField', 'ModelMultipleChoiceField'
       
    22 )
       
    23 
       
    24 def save_instance(form, instance, fields=None, fail_message='saved',
       
    25                   commit=True):
       
    26     """
       
    27     Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
       
    28 
       
    29     If commit=True, then the changes to ``instance`` will be saved to the
       
    30     database. Returns ``instance``.
       
    31     """
       
    32     from django.db import models
       
    33     opts = instance.__class__._meta
       
    34     if form.errors:
       
    35         raise ValueError("The %s could not be %s because the data didn't"
       
    36                          " validate." % (opts.object_name, fail_message))
       
    37     cleaned_data = form.cleaned_data
       
    38     for f in opts.fields:
       
    39         if not f.editable or isinstance(f, models.AutoField) \
       
    40                 or not f.name in cleaned_data:
       
    41             continue
       
    42         if fields and f.name not in fields:
       
    43             continue
       
    44         f.save_form_data(instance, cleaned_data[f.name])
       
    45     # Wrap up the saving of m2m data as a function.
       
    46     def save_m2m():
       
    47         opts = instance.__class__._meta
       
    48         cleaned_data = form.cleaned_data
       
    49         for f in opts.many_to_many:
       
    50             if fields and f.name not in fields:
       
    51                 continue
       
    52             if f.name in cleaned_data:
       
    53                 f.save_form_data(instance, cleaned_data[f.name])
       
    54     if commit:
       
    55         # If we are committing, save the instance and the m2m data immediately.
       
    56         instance.save()
       
    57         save_m2m()
       
    58     else:
       
    59         # We're not committing. Add a method to the form to allow deferred
       
    60         # saving of m2m data.
       
    61         form.save_m2m = save_m2m
       
    62     return instance
       
    63 
       
    64 def make_model_save(model, fields, fail_message):
       
    65     """Returns the save() method for a Form."""
       
    66     def save(self, commit=True):
       
    67         return save_instance(self, model(), fields, fail_message, commit)
       
    68     return save
       
    69 
       
    70 def make_instance_save(instance, fields, fail_message):
       
    71     """Returns the save() method for a Form."""
       
    72     def save(self, commit=True):
       
    73         return save_instance(self, instance, fields, fail_message, commit)
       
    74     return save
       
    75 
       
    76 def form_for_model(model, form=BaseForm, fields=None,
       
    77                    formfield_callback=lambda f: f.formfield()):
       
    78     """
       
    79     Returns a Form class for the given Django model class.
       
    80 
       
    81     Provide ``form`` if you want to use a custom BaseForm subclass.
       
    82 
       
    83     Provide ``formfield_callback`` if you want to define different logic for
       
    84     determining the formfield for a given database field. It's a callable that
       
    85     takes a database Field instance and returns a form Field instance.
       
    86     """
       
    87     warn("form_for_model is deprecated. Use ModelForm instead.",
       
    88         PendingDeprecationWarning, stacklevel=3)
       
    89     opts = model._meta
       
    90     field_list = []
       
    91     for f in opts.fields + opts.many_to_many:
       
    92         if not f.editable:
       
    93             continue
       
    94         if fields and not f.name in fields:
       
    95             continue
       
    96         formfield = formfield_callback(f)
       
    97         if formfield:
       
    98             field_list.append((f.name, formfield))
       
    99     base_fields = SortedDict(field_list)
       
   100     return type(opts.object_name + 'Form', (form,),
       
   101         {'base_fields': base_fields, '_model': model,
       
   102          'save': make_model_save(model, fields, 'created')})
       
   103 
       
   104 def form_for_instance(instance, form=BaseForm, fields=None,
       
   105                       formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
       
   106     """
       
   107     Returns a Form class for the given Django model instance.
       
   108 
       
   109     Provide ``form`` if you want to use a custom BaseForm subclass.
       
   110 
       
   111     Provide ``formfield_callback`` if you want to define different logic for
       
   112     determining the formfield for a given database field. It's a callable that
       
   113     takes a database Field instance, plus **kwargs, and returns a form Field
       
   114     instance with the given kwargs (i.e. 'initial').
       
   115     """
       
   116     warn("form_for_instance is deprecated. Use ModelForm instead.",
       
   117         PendingDeprecationWarning, stacklevel=3)
       
   118     model = instance.__class__
       
   119     opts = model._meta
       
   120     field_list = []
       
   121     for f in opts.fields + opts.many_to_many:
       
   122         if not f.editable:
       
   123             continue
       
   124         if fields and not f.name in fields:
       
   125             continue
       
   126         current_value = f.value_from_object(instance)
       
   127         formfield = formfield_callback(f, initial=current_value)
       
   128         if formfield:
       
   129             field_list.append((f.name, formfield))
       
   130     base_fields = SortedDict(field_list)
       
   131     return type(opts.object_name + 'InstanceForm', (form,),
       
   132         {'base_fields': base_fields, '_model': model,
       
   133          'save': make_instance_save(instance, fields, 'changed')})
       
   134 
       
   135 def form_for_fields(field_list):
       
   136     """
       
   137     Returns a Form class for the given list of Django database field instances.
       
   138     """
       
   139     fields = SortedDict([(f.name, f.formfield())
       
   140                          for f in field_list if f.editable])
       
   141     return type('FormForFields', (BaseForm,), {'base_fields': fields})
       
   142 
       
   143 
       
   144 # ModelForms #################################################################
       
   145 
       
   146 def model_to_dict(instance, fields=None, exclude=None):
       
   147     """
       
   148     Returns a dict containing the data in ``instance`` suitable for passing as
       
   149     a Form's ``initial`` keyword argument.
       
   150 
       
   151     ``fields`` is an optional list of field names. If provided, only the named
       
   152     fields will be included in the returned dict.
       
   153 
       
   154     ``exclude`` is an optional list of field names. If provided, the named
       
   155     fields will be excluded from the returned dict, even if they are listed in
       
   156     the ``fields`` argument.
       
   157     """
       
   158     # avoid a circular import
       
   159     from django.db.models.fields.related import ManyToManyField
       
   160     opts = instance._meta
       
   161     data = {}
       
   162     for f in opts.fields + opts.many_to_many:
       
   163         if not f.editable:
       
   164             continue
       
   165         if fields and not f.name in fields:
       
   166             continue
       
   167         if exclude and f.name in exclude:
       
   168             continue
       
   169         if isinstance(f, ManyToManyField):
       
   170             # If the object doesn't have a primry key yet, just use an empty
       
   171             # list for its m2m fields. Calling f.value_from_object will raise
       
   172             # an exception.
       
   173             if instance.pk is None:
       
   174                 data[f.name] = []
       
   175             else:
       
   176                 # MultipleChoiceWidget needs a list of pks, not object instances.
       
   177                 data[f.name] = [obj.pk for obj in f.value_from_object(instance)]
       
   178         else:
       
   179             data[f.name] = f.value_from_object(instance)
       
   180     return data
       
   181 
       
   182 def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda f: f.formfield()):
       
   183     """
       
   184     Returns a ``SortedDict`` containing form fields for the given model.
       
   185 
       
   186     ``fields`` is an optional list of field names. If provided, only the named
       
   187     fields will be included in the returned fields.
       
   188 
       
   189     ``exclude`` is an optional list of field names. If provided, the named
       
   190     fields will be excluded from the returned fields, even if they are listed
       
   191     in the ``fields`` argument.
       
   192     """
       
   193     # TODO: if fields is provided, it would be nice to return fields in that order
       
   194     field_list = []
       
   195     opts = model._meta
       
   196     for f in opts.fields + opts.many_to_many:
       
   197         if not f.editable:
       
   198             continue
       
   199         if fields and not f.name in fields:
       
   200             continue
       
   201         if exclude and f.name in exclude:
       
   202             continue
       
   203         formfield = formfield_callback(f)
       
   204         if formfield:
       
   205             field_list.append((f.name, formfield))
       
   206     return SortedDict(field_list)
       
   207 
       
   208 class ModelFormOptions(object):
       
   209     def __init__(self, options=None):
       
   210         self.model = getattr(options, 'model', None)
       
   211         self.fields = getattr(options, 'fields', None)
       
   212         self.exclude = getattr(options, 'exclude', None)
       
   213 
       
   214 
       
   215 class ModelFormMetaclass(type):
       
   216     def __new__(cls, name, bases, attrs,
       
   217                 formfield_callback=lambda f: f.formfield()):
       
   218         try:
       
   219             parents = [b for b in bases if issubclass(b, ModelForm)]
       
   220         except NameError:
       
   221             # We are defining ModelForm itself.
       
   222             parents = None
       
   223         if not parents:
       
   224             return super(ModelFormMetaclass, cls).__new__(cls, name, bases,
       
   225                     attrs)
       
   226 
       
   227         new_class = type.__new__(cls, name, bases, attrs)
       
   228         declared_fields = get_declared_fields(bases, attrs, False)
       
   229         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
       
   230         if opts.model:
       
   231             # If a model is defined, extract form fields from it.
       
   232             fields = fields_for_model(opts.model, opts.fields,
       
   233                                       opts.exclude, formfield_callback)
       
   234             # Override default model fields with any custom declared ones
       
   235             # (plus, include all the other declared fields).
       
   236             fields.update(declared_fields)
       
   237         else:
       
   238             fields = declared_fields
       
   239         new_class.declared_fields = declared_fields
       
   240         new_class.base_fields = fields
       
   241         return new_class
       
   242 
       
   243 class BaseModelForm(BaseForm):
       
   244     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
       
   245                  initial=None, error_class=ErrorList, label_suffix=':',
       
   246                  instance=None):
       
   247         opts = self._meta
       
   248         if instance is None:
       
   249             # if we didn't get an instance, instantiate a new one
       
   250             self.instance = opts.model()
       
   251             object_data = {}
       
   252         else:
       
   253             self.instance = instance
       
   254             object_data = model_to_dict(instance, opts.fields, opts.exclude)
       
   255         # if initial was provided, it should override the values from instance
       
   256         if initial is not None:
       
   257             object_data.update(initial)
       
   258         BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix)
       
   259 
       
   260     def save(self, commit=True):
       
   261         """
       
   262         Saves this ``form``'s cleaned_data into model instance
       
   263         ``self.instance``.
       
   264 
       
   265         If commit=True, then the changes to ``instance`` will be saved to the
       
   266         database. Returns ``instance``.
       
   267         """
       
   268         if self.instance.pk is None:
       
   269             fail_message = 'created'
       
   270         else:
       
   271             fail_message = 'changed'
       
   272         return save_instance(self, self.instance, self._meta.fields, fail_message, commit)
       
   273 
       
   274 class ModelForm(BaseModelForm):
       
   275     __metaclass__ = ModelFormMetaclass
       
   276 
       
   277 
       
   278 # Fields #####################################################################
       
   279 
       
   280 class ModelChoiceIterator(object):
       
   281     def __init__(self, field):
       
   282         self.field = field
       
   283         self.queryset = field.queryset
       
   284 
       
   285     def __iter__(self):
       
   286         if self.field.empty_label is not None:
       
   287             yield (u"", self.field.empty_label)
       
   288         for obj in self.queryset:
       
   289             yield (obj.pk, self.field.label_from_instance(obj))
       
   290         # Clear the QuerySet cache if required.
       
   291         if not self.field.cache_choices:
       
   292             self.queryset._result_cache = None
       
   293 
       
   294 class ModelChoiceField(ChoiceField):
       
   295     """A ChoiceField whose choices are a model QuerySet."""
       
   296     # This class is a subclass of ChoiceField for purity, but it doesn't
       
   297     # actually use any of ChoiceField's implementation.
       
   298     default_error_messages = {
       
   299         'invalid_choice': _(u'Select a valid choice. That choice is not one of'
       
   300                             u' the available choices.'),
       
   301     }
       
   302 
       
   303     def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
       
   304                  required=True, widget=Select, label=None, initial=None,
       
   305                  help_text=None, *args, **kwargs):
       
   306         self.empty_label = empty_label
       
   307         self.cache_choices = cache_choices
       
   308         
       
   309         # Call Field instead of ChoiceField __init__() because we don't need
       
   310         # ChoiceField.__init__().
       
   311         Field.__init__(self, required, widget, label, initial, help_text,
       
   312                        *args, **kwargs)
       
   313         self.queryset = queryset
       
   314 
       
   315     def _get_queryset(self):
       
   316         return self._queryset
       
   317 
       
   318     def _set_queryset(self, queryset):
       
   319         self._queryset = queryset
       
   320         self.widget.choices = self.choices
       
   321 
       
   322     queryset = property(_get_queryset, _set_queryset)
       
   323 
       
   324     # this method will be used to create object labels by the QuerySetIterator. 
       
   325     # Override it to customize the label. 
       
   326     def label_from_instance(self, obj):
       
   327         """
       
   328         This method is used to convert objects into strings; it's used to
       
   329         generate the labels for the choices presented by this object. Subclasses
       
   330         can override this method to customize the display of the choices.
       
   331         """
       
   332         return smart_unicode(obj)
       
   333     
       
   334     def _get_choices(self):
       
   335         # If self._choices is set, then somebody must have manually set
       
   336         # the property self.choices. In this case, just return self._choices.
       
   337         if hasattr(self, '_choices'):
       
   338             return self._choices
       
   339 
       
   340         # Otherwise, execute the QuerySet in self.queryset to determine the
       
   341         # choices dynamically. Return a fresh QuerySetIterator that has not been
       
   342         # consumed. Note that we're instantiating a new QuerySetIterator *each*
       
   343         # time _get_choices() is called (and, thus, each time self.choices is
       
   344         # accessed) so that we can ensure the QuerySet has not been consumed. This
       
   345         # construct might look complicated but it allows for lazy evaluation of
       
   346         # the queryset.
       
   347         return ModelChoiceIterator(self)
       
   348 
       
   349     def _set_choices(self, value):
       
   350         # This method is copied from ChoiceField._set_choices(). It's necessary
       
   351         # because property() doesn't allow a subclass to overwrite only
       
   352         # _get_choices without implementing _set_choices.
       
   353         self._choices = self.widget.choices = list(value)
       
   354 
       
   355     choices = property(_get_choices, _set_choices)
       
   356 
       
   357     def clean(self, value):
       
   358         Field.clean(self, value)
       
   359         if value in EMPTY_VALUES:
       
   360             return None
       
   361         try:
       
   362             value = self.queryset.get(pk=value)
       
   363         except self.queryset.model.DoesNotExist:
       
   364             raise ValidationError(self.error_messages['invalid_choice'])
       
   365         return value
       
   366 
       
   367 class ModelMultipleChoiceField(ModelChoiceField):
       
   368     """A MultipleChoiceField whose choices are a model QuerySet."""
       
   369     hidden_widget = MultipleHiddenInput
       
   370     default_error_messages = {
       
   371         'list': _(u'Enter a list of values.'),
       
   372         'invalid_choice': _(u'Select a valid choice. %s is not one of the'
       
   373                             u' available choices.'),
       
   374     }
       
   375 
       
   376     def __init__(self, queryset, cache_choices=False, required=True,
       
   377                  widget=SelectMultiple, label=None, initial=None,
       
   378                  help_text=None, *args, **kwargs):
       
   379         super(ModelMultipleChoiceField, self).__init__(queryset, None,
       
   380             cache_choices, required, widget, label, initial, help_text,
       
   381             *args, **kwargs)
       
   382 
       
   383     def clean(self, value):
       
   384         if self.required and not value:
       
   385             raise ValidationError(self.error_messages['required'])
       
   386         elif not self.required and not value:
       
   387             return []
       
   388         if not isinstance(value, (list, tuple)):
       
   389             raise ValidationError(self.error_messages['list'])
       
   390         final_values = []
       
   391         for val in value:
       
   392             try:
       
   393                 obj = self.queryset.get(pk=val)
       
   394             except self.queryset.model.DoesNotExist:
       
   395                 raise ValidationError(self.error_messages['invalid_choice'] % val)
       
   396             else:
       
   397                 final_values.append(obj)
       
   398         return final_values