From 6aa5091d58d4f9ad18601b56d39d0de5af094f52 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 12 May 2007 14:42:46 +0000 Subject: [PATCH] Added docs for form_for_model and form_for_instance, and added a fields argument so it is easy to create forms from a subset of model fields. git-svn-id: http://code.djangoproject.com/svn/django/trunk@5202 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/newforms/models.py | 47 ++++---- docs/newforms.txt | 155 +++++++++++++++++++++++++ tests/modeltests/model_forms/models.py | 30 ++++- 3 files changed, 210 insertions(+), 22 deletions(-) diff --git a/django/newforms/models.py b/django/newforms/models.py index a60002b705..ea99ca9ea0 100644 --- a/django/newforms/models.py +++ b/django/newforms/models.py @@ -12,17 +12,7 @@ from widgets import Select, SelectMultiple, MultipleHiddenInput __all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', 'ModelChoiceField', 'ModelMultipleChoiceField') -def model_save(self, commit=True): - """ - Creates and returns model instance according to self.clean_data. - - This method is created for any form_for_model Form. - """ - if self.errors: - raise ValueError("The %s could not be created because the data didn't validate." % self._model._meta.object_name) - return save_instance(self, self._model(), commit) - -def save_instance(form, instance, commit=True): +def save_instance(form, instance, fields=None, fail_message='saved', commit=True): """ Saves bound Form ``form``'s clean_data into model instance ``instance``. @@ -33,15 +23,19 @@ def save_instance(form, instance, commit=True): from django.db import models opts = instance.__class__._meta if form.errors: - raise ValueError("The %s could not be changed because the data didn't validate." % opts.object_name) + raise ValueError("The %s could not be %s because the data didn't validate." % (opts.object_name, fail_message)) clean_data = form.clean_data for f in opts.fields: if not f.editable or isinstance(f, models.AutoField) or not f.name in clean_data: continue + if fields and f.name not in fields: + continue setattr(instance, f.name, clean_data[f.name]) if commit: instance.save() for f in opts.many_to_many: + if fields and f.name not in fields: + continue if f.name in clean_data: setattr(instance, f.attname, clean_data[f.name]) # GOTCHA: If many-to-many data is given and commit=False, the many-to-many @@ -50,13 +44,19 @@ def save_instance(form, instance, commit=True): # exception in that case. return instance -def make_instance_save(instance): - "Returns the save() method for a form_for_instance Form." +def make_model_save(model, fields, fail_message): + "Returns the save() method for a Form." def save(self, commit=True): - return save_instance(self, instance, commit) + 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, formfield_callback=lambda f: f.formfield()): +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. @@ -71,13 +71,16 @@ def form_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfiel 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)) - fields = SortedDictFromList(field_list) - return type(opts.object_name + 'Form', (form,), {'base_fields': fields, '_model': model, 'save': model_save}) + base_fields = SortedDictFromList(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, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): +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. @@ -94,13 +97,15 @@ def form_for_instance(instance, form=BaseForm, formfield_callback=lambda f, **kw 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)) - fields = SortedDictFromList(field_list) + base_fields = SortedDictFromList(field_list) return type(opts.object_name + 'InstanceForm', (form,), - {'base_fields': fields, '_model': model, 'save': make_instance_save(instance)}) + {'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." diff --git a/docs/newforms.txt b/docs/newforms.txt index ddb850f54c..5d19eeac3a 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -870,6 +870,161 @@ custom ``Field`` classes. To do this, just create a subclass of mentioned above (``required``, ``label``, ``initial``, ``widget``, ``help_text``). +Generating forms for models +=========================== + +Although you can build customized forms by specifying the fields manually, +in many cases you won't need to. Django provides helper methods to simplify the +common cases of form creation. + +``form_for_model()`` +-------------------- + +This method creates a form based upon the definition for a specific model. +``form_for_model()`` examines the model definition, and creates a new form +class that contains a form field for each model field that is defined. + +The type of fields produced on the generated form is determined by the type +of the model fields. For example, a ``CharField`` on a model will be +represented with a ``CharField`` on the form. Each ``ManyToManyField`` +on the model will be represented with a ``MultipleChoiceField`` on the +form. Each ``ForeignKey`` will be represented with a ``ChoiceField``. +A ``ChoiceField`` is also used for any model field that has a ``choices`` +attribute specified. + +``form_for_model()`` returns a generated class. This class must then be +instantiated:: + + # Create the form class + >>> ArticleForm = form_for_model(Article) + + # Create an empty form instance + >>> f = ArticleForm() + +The form produced by ``form_for_model`` also has a ``save()`` method. Once the +form contains valid data, the ``save()`` method can be used to create a model +instance with the attribute values described on the form:: + + # Create a form instance populated with POST data + >>> f = ArticleForm(request.POST) + + # Save the new instance + >>> new_article = f.save() + +Using an alternate base class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to add other methods to the generated form, you can put those +methods onto a base class, and instruct ``form_for_model()`` to use that +base class. + +By default, every form produced by ``form_for_model()`` extends +``django.newforms.forms.BaseForm``. However, if you provide a ``forms`` +argument to ``form_for_model()``, Django will use that class as the base +for the form it generates:: + + # Create the new base class: + >>> class MyBase(BaseForm): + ... def fiddle(self): + ... # Do whatever the method does + + # Create the form class with a different base class + >>> ArticleForm = form_for_model(Article, form=MyBase) + + # Instantiate the form + >>> f = ArticleForm() + + # Use the base class method + >>> f.fiddle() + +Putting a subset of fields on the form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +**New in Django development version** + +In some cases, you may not want all the model fields to appear on the form. +One option is to set ``editable=False`` on the model field. ``form_for_model()`` +will not include any non-editable fields on a generated form instance. + +However, if you just want to exclude a field from one specific form, you +can use the ``fields`` argument. If you provide a fields argument to +``form_for_model()``, only the fields named will be included on the form. +For example, if you only want the 'title' and 'pub_date' attributes to be +included on the Article form, you would call:: + + >>> PartialArticleForm = form_for_model(Article, fields=('title', 'pub_date')) + +.. note:: + If you specify ``fields`` when creating a form with ``form_for_model()`` + make sure that the fields that are *not* specified can provide default + values, or are allowed to have a value of ``None``. If a field isn't + specified on a form, the object created from the form can't provide + a value for that attribute, which will prevent the new instance from + being saved. + +Overriding the default field types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Although the form field types generated by ``form_for_model()`` are suitable +for most general purposes, you may have need to override the default field +types on a specific form. In order to do this, ``form_for_model()`` provides +access to the *formfield callback*. + +The formfield callback is a function that, when provided with a model field, +returns a form field instance. When constructing a form, ``form_for_model()`` +asks the formfield callback to provide form field types. The default +implementation asks the model field for an appropriate field type; however, +any other strategy may be employed. If you need to use an alternate strategy, +you can define your own callback, and provide it to ``form_for_model()`` using +the ``formfield_callback`` argument. + +For example, if you wanted to use ``MyDateFormField`` for any ``DateField`` +fields on the model, you could define the callback:: + + >>> def my_fields(field, **kwargs): + ... if isinstance(field, models.DateField): + ... return MyDateFormField(**kwargs) + ... else: + ... return field.formfield(**kwargs) + + >>> ArticleForm = form_for_model(formfield_callback=my_fields) + +Note that your callback needs to handle *all* possible model field types, not +just the ones that you want to behave differently to the default. + +Finding the model associated with a form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The model class that was used to construct the form is available +using the ``_model`` property of the generated form. + +``form_for_instance()`` +----------------------- + +``form_for_instance()`` is very similar to ``form_for_model()``. However, +rather than using a model class to generate a form, it uses an instance of a +model:: + + # Create an article + >>> art = Article(... some data ...) + >>> art.save() + + # Create a form + >>> ArticleForm = form_for_instance(art) + + # Instantiate the form + >>> f = ArticleForm() + +When a form created by ``form_for_instance()`` is created, the initial +data values for the form fields are drawn from the instance. However, +this data is not bound to the form. You will need to bind data to the +form before the form can be saved. + +When you call ``save()`` on a form created by ``form_for_instance()``, +the database instance will be updated. + +``form_for_instance()`` has ``form``, ``fields`` and ``formfield_callback`` +arguments that behave the same way as they do for ``form_for_model()``. + More coming soon ================ diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index a23529b566..e78f5f7bd7 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -179,6 +179,18 @@ fields with the 'choices' attribute are represented by a ChoiceField.
Hold down "Control", or "Command" on a Mac, to select more than one. +You can restrict a form to a subset of the complete list of fields +by providing a 'fields' argument. If you try to save a +model created with such a form, you need to ensure that the fields +that are _not_ on the form have default values, or are allowed to have +a value of None. If a field isn't specified on a form, the object created +from the form can't provide a value for that field! +>>> PartialArticleForm = form_for_model(Article, fields=('headline','pub_date')) +>>> f = PartialArticleForm(auto_id=False) +>>> print f +Headline: +Pub date: + You can pass a custom Form class to form_for_model. Make sure it's a subclass of BaseForm, not Form. >>> class CustomForm(BaseForm): @@ -224,7 +236,23 @@ current values are inserted as 'initial' data in each Field. Hold down "Control", or "Command" on a Mac, to select more than one. ->>> f = TestArticleForm({'headline': u'New headline', 'pub_date': u'1988-01-04', 'writer': u'1', 'article': 'Hello.'}) +>>> f = TestArticleForm({'headline': u'Test headline', 'pub_date': u'1984-02-06', 'writer': u'1', 'article': 'Hello.'}) +>>> f.is_valid() +True +>>> test_art = f.save() +>>> test_art.id +1 +>>> test_art = Article.objects.get(id=1) +>>> test_art.headline +'Test headline' + +You can create a form over a subset of the available fields +by specifying a 'fields' argument to form_for_instance. +>>> PartialArticleForm = form_for_instance(art, fields=('headline','pub_date')) +>>> f = PartialArticleForm({'headline': u'New headline', 'pub_date': u'1988-01-04'}, auto_id=False) +>>> print f.as_ul() +
  • Headline:
  • +
  • Pub date:
  • >>> f.is_valid() True >>> new_art = f.save()