From a19ed8aea395e8e07164ff7d85bd7dff2f24edca Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 18 Jul 2008 23:54:34 +0000 Subject: Merged the newforms-admin branch into trunk. This is a backward incompatible change. The admin contrib app has been refactored. The newforms module has several improvements including FormSets and Media definitions. git-svn-id: http://code.djangoproject.com/svn/django/trunk@7967 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/admin.txt | 678 +++++++++++++++++++++++++++++++++++++++++++ docs/authentication.txt | 44 +-- docs/custom_model_fields.txt | 1 - docs/localflavor.txt | 4 +- docs/model-api.txt | 478 ------------------------------ docs/modelforms.txt | 122 ++++++++ docs/newforms.txt | 640 ++++++++++++++++++++++++++++++++++++++++ docs/tutorial02.txt | 163 +++++------ 8 files changed, 1546 insertions(+), 584 deletions(-) create mode 100644 docs/admin.txt (limited to 'docs') diff --git a/docs/admin.txt b/docs/admin.txt new file mode 100644 index 0000000000..fbdd19bc90 --- /dev/null +++ b/docs/admin.txt @@ -0,0 +1,678 @@ +===================== +The Django admin site +===================== + +One of the most powerful parts of Django is the automatic admin interface. It +reads metadata in your model to provide a powerful and production-ready +interface that content producers can immediately use to start adding content to +the site. In this document, we discuss how to activate, use and customize +Django's admin interface. + +.. admonition:: Note + + The admin site has been refactored significantly since Django 0.96. This + document describes the newest version of the admin site, which allows for + much richer customization. If you follow the development of Django itself, + you may have heard this described as "newforms-admin." + +Overview +======== + +There are four steps in activating the Django admin site: + + 1. Determine which of your application's models should be editable in the + admin interface. + + 2. For each of those models, optionally create a ``ModelAdmin`` class that + encapsulates the customized admin functionality and options for that + particular model. + + 3. Instantiate an ``AdminSite`` and tell it about each of your models and + ``ModelAdmin`` classes. + + 4. Hook the ``AdminSite`` instance into your URLconf. + +``ModelAdmin`` objects +====================== + +The ``ModelAdmin`` class is the representation of a model in the admin +interface. These are stored in a file named ``admin.py`` in your application. +Let's take a look at a very simple example the ``ModelAdmin``:: + + from django.contrib import admin + from myproject.myapp.models import Author + + class AuthorAdmin(admin.ModelAdmin): + pass + admin.site.register(Author, AuthorAdmin) + +``ModelAdmin`` Options +---------------------- + +The ``ModelAdmin`` is very flexible. It has several options for dealing with +customizing the interface. All options are defined on the ``ModelAdmin`` +subclass:: + + class AuthorAdmin(admin.ModelAdmin): + date_hierarchy = 'pub_date' + +``date_hierarchy`` +~~~~~~~~~~~~~~~~~~ + +Set ``date_hierarchy`` to the name of a ``DateField`` or ``DateTimeField`` in +your model, and the change list page will include a date-based drilldown +navigation by that field. + +Example:: + + date_hierarchy = 'pub_date' + +``fieldsets`` +~~~~~~~~~~~~~ + +Set ``fieldsets`` to control the layout of admin "add" and "change" pages. + +``fieldsets`` is a list of two-tuples, in which each two-tuple represents a +``
`` on the admin form page. (A ``
`` is a "section" of the +form.) + +The two-tuples are in the format ``(name, field_options)``, where ``name`` is a +string representing the title of the fieldset and ``field_options`` is a +dictionary of information about the fieldset, including a list of fields to be +displayed in it. + +A full example, taken from the ``django.contrib.flatpages.FlatPage`` model:: + + class FlatPageAdmin(admin.ModelAdmin): + fieldsets = ( + (None, { + 'fields': ('url', 'title', 'content', 'sites') + }), + ('Advanced options', { + 'classes': ('collapse',), + 'fields': ('enable_comments', 'registration_required', 'template_name') + }), + ) + +This results in an admin page that looks like: + + .. image:: http://media.djangoproject.com/img/doc/flatfiles_admin.png + +If ``fieldsets`` isn't given, Django will default to displaying each field +that isn't an ``AutoField`` and has ``editable=True``, in a single fieldset, +in the same order as the fields are defined in the model. + +The ``field_options`` dictionary can have the following keys: + +``fields`` + A tuple of field names to display in this fieldset. This key is required. + + Example:: + + { + 'fields': ('first_name', 'last_name', 'address', 'city', 'state'), + } + + To display multiple fields on the same line, wrap those fields in their own + tuple. In this example, the ``first_name`` and ``last_name`` fields will + display on the same line:: + + { + 'fields': (('first_name', 'last_name'), 'address', 'city', 'state'), + } + +``classes`` + A string containing extra CSS classes to apply to the fieldset. + + Example:: + + { + 'classes': 'wide', + } + + Apply multiple classes by separating them with spaces. Example:: + + { + 'classes': 'wide extrapretty', + } + + Two useful classes defined by the default admin-site stylesheet are + ``collapse`` and ``wide``. Fieldsets with the ``collapse`` style will be + initially collapsed in the admin and replaced with a small "click to expand" + link. Fieldsets with the ``wide`` style will be given extra horizontal space. + +``description`` + A string of optional extra text to be displayed at the top of each fieldset, + under the heading of the fieldset. It's used verbatim, so you can use any HTML + and you must escape any special HTML characters (such as ampersands) yourself. + +``filter_horizontal`` +~~~~~~~~~~~~~~~~~~~~~ + +Use a nifty unobtrusive Javascript "filter" interface instead of the +usability-challenged ``) for +fields that are ``ForeignKey`` or have ``choices`` set. If a field is present +in ``radio_fields``, Django will use a radio-button interface instead. +Assuming ``group`` is a ``ForeignKey`` on the ``Person`` model:: + + class PersonAdmin(admin.ModelAdmin): + radio_fields = {"group": admin.VERTICAL} + +You have the choice of using ``HORIZONTAL`` or ``VERTICAL`` from the +``django.contrib.admin`` module. + +Don't include a field in ``radio_fields`` unless it's a ``ForeignKey`` or has +``choices`` set. + +``raw_id_fields`` +~~~~~~~~~~~~~~~~~ + +By default, Django's admin uses a select-box interface ( @@ -557,7 +557,7 @@ block:: {% endblock %} -.. _forms documentation: ../forms/ +.. _newforms documentation: ../newforms/ .. _site framework docs: ../sites/ Other built-in views @@ -677,29 +677,29 @@ successful login. * ``login_url``: The URL of the login page to redirect to. This will default to ``settings.LOGIN_URL`` if not supplied. -Built-in manipulators ---------------------- +Built-in forms +-------------- + +**New in Django development version.** If you don't want to use the built-in views, but want the convenience -of not having to write manipulators for this functionality, the -authentication system provides several built-in manipulators: +of not having to write forms for this functionality, the authentication +system provides several built-in forms: - * ``django.contrib.auth.forms.AdminPasswordChangeForm``: A - manipulator used in the admin interface to change a user's - password. + * ``django.contrib.auth.forms.AdminPasswordChangeForm``: A form used in + the admin interface to change a user's password. - * ``django.contrib.auth.forms.AuthenticationForm``: A manipulator - for logging a user in. + * ``django.contrib.auth.forms.AuthenticationForm``: A form for logging a + user in. - * ``django.contrib.auth.forms.PasswordChangeForm``: A manipulator - for allowing a user to change their password. + * ``django.contrib.auth.forms.PasswordChangeForm``: A form for allowing a + user to change their password. - * ``django.contrib.auth.forms.PasswordResetForm``: A manipulator - for resetting a user's password and emailing the new password to - them. + * ``django.contrib.auth.forms.PasswordResetForm``: A form for resetting a + user's password and emailing the new password to them. - * ``django.contrib.auth.forms.UserCreationForm``: A manipulator - for creating a new user. + * ``django.contrib.auth.forms.UserCreationForm``: A form for creating a + new user. Limiting access to logged-in users that pass a test --------------------------------------------------- diff --git a/docs/custom_model_fields.txt b/docs/custom_model_fields.txt index 2b344921ef..cbaac873e3 100644 --- a/docs/custom_model_fields.txt +++ b/docs/custom_model_fields.txt @@ -204,7 +204,6 @@ order: * ``unique_for_year`` * ``validator_list`` * ``choices`` - * ``radio_admin`` * ``help_text`` * ``db_column`` * ``db_tablespace``: Currently only used with the Oracle backend and only diff --git a/docs/localflavor.txt b/docs/localflavor.txt index b4bccfb138..5a2e5b8fda 100644 --- a/docs/localflavor.txt +++ b/docs/localflavor.txt @@ -20,10 +20,10 @@ For example, here's how you can create a form with a field representing a French telephone number:: from django import newforms as forms - from django.contrib.localflavor.fr.forms import FRPhoneNumberField + from django.contrib.localflavor import fr class MyForm(forms.Form): - my_french_phone_no = FRPhoneNumberField() + my_french_phone_no = fr.forms.FRPhoneNumberField() Supported countries =================== diff --git a/docs/model-api.txt b/docs/model-api.txt index 4e7b5c3096..4accad122a 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -12,8 +12,6 @@ The basics: * Each attribute of the model represents a database field. * Model metadata (non-field information) goes in an inner class named ``Meta``. - * Metadata used for Django's admin site goes into an inner class named - ``Admin``. * With all of this, Django gives you an automatically-generated database-access API, which is explained in the `Database API reference`_. @@ -425,18 +423,6 @@ not specified, Django will use a default length of 50. Implies ``db_index=True``. -Accepts an extra option, ``prepopulate_from``, which is a list of fields -from which to auto-populate the slug, via JavaScript, in the object's admin -form:: - - models.SlugField(prepopulate_from=("pre_name", "name")) - -``prepopulate_from`` doesn't accept DateTimeFields, ForeignKeys nor -ManyToManyFields. - -The admin represents ``SlugField`` as an ```` (a -single-line input). - ``SmallIntegerField`` ~~~~~~~~~~~~~~~~~~~~~ @@ -665,16 +651,6 @@ unless you want to override the default primary-key behavior. ``primary_key=True`` implies ``null=False`` and ``unique=True``. Only one primary key is allowed on an object. -``radio_admin`` -~~~~~~~~~~~~~~~ - -By default, Django's admin uses a select-box interface (`` - in the admin form for this object. The value should be - ``models.HORIZONTAL`` or ``models.VERTICAL`` (i.e. - should the interface be stacked horizontally or - vertically). - ``limit_choices_to`` See the description under ``ForeignKey`` above. ``symmetrical`` Only used in the definition of ManyToManyFields on self. @@ -1255,412 +1183,6 @@ attribute is the primary key field for the model. You can read and set this value, just as you would for any other attribute, and it will update the correct field in the model. -Admin options -============= - -If you want your model to be visible to Django's admin site, give your model an -inner ``"class Admin"``, like so:: - - class Person(models.Model): - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - - class Admin: - # Admin options go here - pass - -The ``Admin`` class tells Django how to display the model in the admin site. - -Here's a list of all possible ``Admin`` options. None of these options are -required. To use an admin interface without specifying any options, use -``pass``, like so:: - - class Admin: - pass - -Adding ``class Admin`` to a model is completely optional. - -``date_hierarchy`` ------------------- - -Set ``date_hierarchy`` to the name of a ``DateField`` or ``DateTimeField`` in -your model, and the change list page will include a date-based drilldown -navigation by that field. - -Example:: - - date_hierarchy = 'pub_date' - -``fields`` ----------- - -Set ``fields`` to control the layout of admin "add" and "change" pages. - -``fields`` is a list of two-tuples, in which each two-tuple represents a -``
`` on the admin form page. (A ``
`` is a "section" of the -form.) - -The two-tuples are in the format ``(name, field_options)``, where ``name`` is a -string representing the title of the fieldset and ``field_options`` is a -dictionary of information about the fieldset, including a list of fields to be -displayed in it. - -A full example, taken from the ``django.contrib.flatpages.FlatPage`` model:: - - class Admin: - fields = ( - (None, { - 'fields': ('url', 'title', 'content', 'sites') - }), - ('Advanced options', { - 'classes': 'collapse', - 'fields' : ('enable_comments', 'registration_required', 'template_name') - }), - ) - -This results in an admin page that looks like: - - .. image:: http://media.djangoproject.com/img/doc/flatfiles_admin.png - -If ``fields`` isn't given, Django will default to displaying each field that -isn't an ``AutoField`` and has ``editable=True``, in a single fieldset, in -the same order as the fields are defined in the model. - -The ``field_options`` dictionary can have the following keys: - -``fields`` -~~~~~~~~~~ - -A tuple of field names to display in this fieldset. This key is required. - -Example:: - - { - 'fields': ('first_name', 'last_name', 'address', 'city', 'state'), - } - -To display multiple fields on the same line, wrap those fields in their own -tuple. In this example, the ``first_name`` and ``last_name`` fields will -display on the same line:: - - { - 'fields': (('first_name', 'last_name'), 'address', 'city', 'state'), - } - -``classes`` -~~~~~~~~~~~ - -A string containing extra CSS classes to apply to the fieldset. - -Example:: - - { - 'classes': 'wide', - } - -Apply multiple classes by separating them with spaces. Example:: - - { - 'classes': 'wide extrapretty', - } - -Two useful classes defined by the default admin-site stylesheet are -``collapse`` and ``wide``. Fieldsets with the ``collapse`` style will be -initially collapsed in the admin and replaced with a small "click to expand" -link. Fieldsets with the ``wide`` style will be given extra horizontal space. - -``description`` -~~~~~~~~~~~~~~~ - -A string of optional extra text to be displayed at the top of each fieldset, -under the heading of the fieldset. It's used verbatim, so you can use any HTML -and you must escape any special HTML characters (such as ampersands) yourself. - -``js`` ------- - -A list of strings representing URLs of JavaScript files to link into the admin -screen via `` + + +Here's a list of all possible ``Media`` options. There are no required options. + +``css`` +~~~~~~~ + +A dictionary describing the CSS files required for various forms of output +media. + +The values in the dictionary should be a tuple/list of file names. See +`the section on media paths`_ for details of how to specify paths to media +files. + +.. _the section on media paths: `Paths in media definitions`_ + +The keys in the dictionary are the output media types. These are the same +types accepted by CSS files in media declarations: 'all', 'aural', 'braille', +'embossed', 'handheld', 'print', 'projection', 'screen', 'tty' and 'tv'. If +you need to have different stylesheets for different media types, provide +a list of CSS files for each output medium. The following example would +provide two CSS options -- one for the screen, and one for print:: + + class Media: + css = { + 'screen': ('pretty.css',), + 'print': ('newspaper.css',) + } + +If a group of CSS files are appropriate for multiple output media types, +the dictionary key can be a comma separated list of output media types. +In the following example, TV's and projectors will have the same media +requirements:: + + class Media: + css = { + 'screen': ('pretty.css',), + 'tv,projector': ('lo_res.css',), + 'print': ('newspaper.css',) + } + +If this last CSS definition were to be rendered, it would become the following HTML:: + + + + + +``js`` +~~~~~~ + +A tuple describing the required javascript files. See +`the section on media paths`_ for details of how to specify paths to media +files. + +``extend`` +~~~~~~~~~~ + +A boolean defining inheritance behavior for media declarations. + +By default, any object using a static media definition will inherit all the +media associated with the parent widget. This occurs regardless of how the +parent defines its media requirements. For example, if we were to extend our +basic Calendar widget from the example above:: + + class FancyCalendarWidget(CalendarWidget): + class Media: + css = { + 'all': ('fancy.css',) + } + js = ('whizbang.js',) + + >>> w = FancyCalendarWidget() + >>> print w.media + + + + + + +The FancyCalendar widget inherits all the media from it's parent widget. If +you don't want media to be inherited in this way, add an ``extend=False`` +declaration to the media declaration:: + + class FancyCalendar(Calendar): + class Media: + extend = False + css = { + 'all': ('fancy.css',) + } + js = ('whizbang.js',) + + >>> w = FancyCalendarWidget() + >>> print w.media + + + +If you require even more control over media inheritance, define your media +using a `dynamic property`_. Dynamic properties give you complete control over +which media files are inherited, and which are not. + +.. _dynamic property: `Media as a dynamic property`_ + +Media as a dynamic property +--------------------------- + +If you need to perform some more sophisticated manipulation of media +requirements, you can define the media property directly. This is done +by defining a model property that returns an instance of ``forms.Media``. +The constructor for ``forms.Media`` accepts ``css`` and ``js`` keyword +arguments in the same format as that used in a static media definition. + +For example, the static media definition for our Calendar Widget could +also be defined in a dynamic fashion:: + + class CalendarWidget(forms.TextInput): + def _media(self): + return forms.Media(css={'all': ('pretty.css',)}, + js=('animations.js', 'actions.js')) + media = property(_media) + +See the section on `Media objects`_ for more details on how to construct +return values for dynamic media properties. + +Paths in media definitions +-------------------------- + +Paths used to specify media can be either relative or absolute. If a path +starts with '/', 'http://' or 'https://', it will be interpreted as an absolute +path, and left as-is. All other paths will be prepended with the value of +``settings.MEDIA_URL``. For example, if the MEDIA_URL for your site was +``http://media.example.com/``:: + + class CalendarWidget(forms.TextInput): + class Media: + css = { + 'all': ('/css/pretty.css',), + } + js = ('animations.js', 'http://othersite.com/actions.js') + + >>> w = CalendarWidget() + >>> print w.media + + + + +Media objects +------------- + +When you interrogate the media attribute of a widget or form, the value that +is returned is a ``forms.Media`` object. As we have already seen, the string +representation of a Media object is the HTML required to include media +in the ```` block of your HTML page. + +However, Media objects have some other interesting properties. + +Media subsets +~~~~~~~~~~~~~ + +If you only want media of a particular type, you can use the subscript operator +to filter out a medium of interest. For example:: + + >>> w = CalendarWidget() + >>> print w.media + + + + + >>> print w.media['css'] + + +When you use the subscript operator, the value that is returned is a new +Media object -- but one that only contains the media of interest. + +Combining media objects +~~~~~~~~~~~~~~~~~~~~~~~ + +Media objects can also be added together. When two media objects are added, +the resulting Media object contains the union of the media from both files:: + + class CalendarWidget(forms.TextInput): + class Media: + css = { + 'all': ('pretty.css',) + } + js = ('animations.js', 'actions.js') + + class OtherWidget(forms.TextInput): + class Media: + js = ('whizbang.js',) + + >>> w1 = CalendarWidget() + >>> w2 = OtherWidget() + >>> print w1+w2 + + + + + +Media on Forms +-------------- + +Widgets aren't the only objects that can have media definitions -- forms +can also define media. The rules for media definitions on forms are the +same as the rules for widgets: declarations can be static or dynamic; +path and inheritance rules for those declarations are exactly the same. + +Regardless of whether you define a media declaration, *all* Form objects +have a media property. The default value for this property is the result +of adding the media definitions for all widgets that are part of the form:: + + class ContactForm(forms.Form): + date = DateField(widget=CalendarWidget) + name = CharField(max_length=40, widget=OtherWidget) + + >>> f = ContactForm() + >>> f.media + + + + + +If you want to associate additional media with a form -- for example, CSS for form +layout -- simply add a media declaration to the form:: + + class ContactForm(forms.Form): + date = DateField(widget=CalendarWidget) + name = CharField(max_length=40, widget=OtherWidget) + + class Media: + css = { + 'all': ('layout.css',) + } + + >>> f = ContactForm() + >>> f.media + + + + + + +Formsets +======== + +A formset is a layer of abstraction to working with multiple forms on the same +page. It can be best compared to a data grid. Let's say you have the following +form:: + + >>> from django import newforms as forms + >>> class ArticleForm(forms.Form): + ... title = forms.CharField() + ... pub_date = forms.DateField() + +You might want to allow the user to create several articles at once. To create +a formset of ``ArticleForm``s you would do:: + + >>> from django.newforms.formsets import formset_factory + >>> ArticleFormSet = formset_factory(ArticleForm) + +You now have created a formset named ``ArticleFormSet``. The formset gives you +the ability to iterate over the forms in the formset and display them as you +would with a regular form:: + + >>> formset = ArticleFormSet() + >>> for form in formset.forms: + ... print form.as_table() + + + +As you can see it only displayed one form. This is because by default the +``formset_factory`` defines one extra form. This can be controlled with the +``extra`` parameter:: + + >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) + +Using initial data with a formset +--------------------------------- + +Initial data is what drives the main usability of a formset. As shown above +you can define the number of extra forms. What this means is that you are +telling the formset how many additional forms to show in addition to the +number of forms it generates from the initial data. Lets take a look at an +example:: + + >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) + >>> formset = ArticleFormSet(initial=[ + ... {'title': u'Django is now open source', + ... 'pub_date': datetime.date.today()}, + ... ]) + + >>> for form in formset.forms: + ... print form.as_table() + + + + + + + +There are now a total of three forms showing above. One for the initial data +that was passed in and two extra forms. Also note that we are passing in a +list of dictionaries as the initial data. + +Limiting the maximum number of forms +------------------------------------ + +The ``max_num`` parameter to ``formset_factory`` gives you the ability to +force the maximum number of forms the formset will display:: + + >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1) + >>> formset = ArticleFormset() + >>> for form in formset.forms: + ... print form.as_table() + + + +The default value of ``max_num`` is ``0`` which is the same as saying put no +limit on the number forms displayed. + +Formset validation +------------------ + +Validation with a formset is about identical to a regular ``Form``. There is +an ``is_valid`` method on the formset to provide a convenient way to validate +each form in the formset:: + + >>> ArticleFormSet = formset_factory(ArticleForm) + >>> formset = ArticleFormSet({}) + >>> formset.is_valid() + True + +We passed in no data to the formset which is resulting in a valid form. The +formset is smart enough to ignore extra forms that were not changed. If we +attempt to provide an article, but fail to do so:: + + >>> data = { + ... 'form-TOTAL_FORMS': u'1', + ... 'form-INITIAL_FORMS': u'1', + ... 'form-0-title': u'Test', + ... 'form-0-pub_date': u'', + ... } + >>> formset = ArticleFormSet(data) + >>> formset.is_valid() + False + >>> formset.errors + [{'pub_date': [u'This field is required.']}] + +As we can see the formset properly performed validation and gave us the +expected errors. + +Understanding the ManagementForm +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You may have noticed the additional data that was required in the formset's +data above. This data is coming from the ``ManagementForm``. This form is +dealt with internally to the formset. If you don't use it, it will result in +an exception:: + + >>> data = { + ... 'form-0-title': u'Test', + ... 'form-0-pub_date': u'', + ... } + >>> formset = ArticleFormSet(data) + Traceback (most recent call last): + ... + django.newforms.util.ValidationError: [u'ManagementForm data is missing or has been tampered with'] + +It is used to keep track of how many form instances are being displayed. If +you are adding new forms via javascript, you should increment the count fields +in this form as well. + +Custom formset validation +~~~~~~~~~~~~~~~~~~~~~~~~~ + +A formset has a ``clean`` method similar to the one on a ``Form`` class. This +is where you define your own validation that deals at the formset level:: + + >>> from django.newforms.formsets import BaseFormSet + + >>> class BaseArticleFormSet(BaseFormSet): + ... def clean(self): + ... raise forms.ValidationError, u'An error occured.' + + >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet) + >>> formset = ArticleFormSet({}) + >>> formset.is_valid() + False + >>> formset.non_form_errors() + [u'An error occured.'] + +The formset ``clean`` method is called after all the ``Form.clean`` methods +have been called. The errors will be found using the ``non_form_errors()`` +method on the formset. + +Dealing with ordering and deletion of forms +------------------------------------------- + +Common use cases with a formset is dealing with ordering and deletion of the +form instances. This has been dealt with for you. The ``formset_factory`` +provides two optional parameters ``can_order`` and ``can_delete`` that will do +the extra work of adding the extra fields and providing simpler ways of +getting to that data. + +``can_order`` +~~~~~~~~~~~~~ + +Default: ``False`` + +Lets create a formset with the ability to order:: + + >>> ArticleFormSet = formset_factory(ArticleForm, can_order=True) + >>> formset = ArticleFormSet(initial=[ + ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, + ... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)}, + ... ]) + >>> for form in formset.forms: + ... print form.as_table() + + + + + + + + + + +This adds an additional field to each form. This new field is named ``ORDER`` +and is an ``forms.IntegerField``. For the forms that came from the initial +data it automatically assigned them a numeric value. Lets look at what will +happen when the user changes these values:: + + >>> data = { + ... 'form-TOTAL_FORMS': u'3', + ... 'form-INITIAL_FORMS': u'2', + ... 'form-0-title': u'Article #1', + ... 'form-0-pub_date': u'2008-05-10', + ... 'form-0-ORDER': u'2', + ... 'form-1-title': u'Article #2', + ... 'form-1-pub_date': u'2008-05-11', + ... 'form-1-ORDER': u'1', + ... 'form-2-title': u'Article #3', + ... 'form-2-pub_date': u'2008-05-01', + ... 'form-2-ORDER': u'0', + ... } + + >>> formset = ArticleFormSet(data, initial=[ + ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, + ... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)}, + ... ]) + >>> formset.is_valid() + True + >>> for form in formset.ordered_forms: + ... print form.cleaned_data + {'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': u'Article #3'} + {'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': u'Article #2'} + {'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': u'Article #1'} + +``can_delete`` +~~~~~~~~~~~~~~ + +Default: ``False`` + +Lets create a formset with the ability to delete:: + + >>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True) + >>> formset = ArticleFormSet(initial=[ + ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, + ... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)}, + ... ]) + >>> for form in formset.forms: + .... print form.as_table() + + + + + + + + + + + +Similar to ``can_order`` this adds a new field to each form named ``DELETE`` +and is a ``forms.BooleanField``. When data comes through marking any of the +delete fields you can access them with ``deleted_forms``:: + + >>> data = { + ... 'form-TOTAL_FORMS': u'3', + ... 'form-INITIAL_FORMS': u'2', + ... 'form-0-title': u'Article #1', + ... 'form-0-pub_date': u'2008-05-10', + ... 'form-0-DELETE': u'on', + ... 'form-1-title': u'Article #2', + ... 'form-1-pub_date': u'2008-05-11', + ... 'form-1-DELETE': u'', + ... 'form-2-title': u'', + ... 'form-2-pub_date': u'', + ... 'form-2-DELETE': u'', + ... } + + >>> formset = ArticleFormSet(data, initial=[ + ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, + ... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)}, + ... ]) + >>> [form.cleaned_data for form in formset.deleted_forms] + [{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': u'Article #1'}] + +Adding additional fields to a formset +------------------------------------- + +If you need to add additional fields to the formset this can be easily +accomplished. The formset base class provides an ``add_fields`` method. You +can simply override this method to add your own fields or even redefine the +default fields/attributes of the order and deletion fields:: + + >>> class BaseArticleFormSet(BaseFormSet): + ... def add_fields(self, form, index): + ... super(BaseArticleFormSet, self).add_fields(form, index) + ... form.fields["my_field"] = forms.CharField() + + >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet) + >>> formset = ArticleFormSet() + >>> for form in formset.forms: + ... print form.as_table() + + + + +Using a formsets in views and templates +--------------------------------------- + +Using a formset inside a view is as easy as using a regular ``Form`` class. +The only thing you will want to be aware of is making sure to use the +management form inside the template. Lets look at a sample view:: + + def manage_articles(request): + ArticleFormSet = formset_factory(ArticleForm) + if request.method == 'POST': + formset = ArticleFormSet(request.POST, request.FILES) + if formset.is_valid(): + # do something with the formset.cleaned_data + else: + formset = ArticleFormSet() + return render_to_response('manage_articles.html', {'formset': formset}) + +The ``manage_articles.html`` template might look like this:: + +
+ {{ formset.management_form }} + + {% for form in formset.forms %} + {{ form }} + {% endfor %} +
+
+ +However the above can be slightly shortcutted and let the formset itself deal +with the management form:: + +
+ + {{ formset }} +
+
+ +The above ends up calling the ``as_table`` method on the formset class. + More coming soon ================ diff --git a/docs/tutorial02.txt b/docs/tutorial02.txt index 42c9800591..c69fd1459f 100644 --- a/docs/tutorial02.txt +++ b/docs/tutorial02.txt @@ -31,10 +31,10 @@ activate the admin site for your installation, do these three things: * Add ``"django.contrib.admin"`` to your ``INSTALLED_APPS`` setting. * Run ``python manage.py syncdb``. Since you have added a new application to ``INSTALLED_APPS``, the database tables need to be updated. - * Edit your ``mysite/urls.py`` file and uncomment the line below - "Uncomment this for admin:". This file is a URLconf; we'll dig into - URLconfs in the next tutorial. For now, all you need to know is that it - maps URL roots to applications. + * Edit your ``mysite/urls.py`` file and uncomment the lines below the + "Uncomment this for admin:" comments. This file is a URLconf; we'll dig + into URLconfs in the next tutorial. For now, all you need to know is that + it maps URL roots to applications. Start the development server ============================ @@ -71,19 +71,13 @@ Make the poll app modifiable in the admin But where's our poll app? It's not displayed on the admin index page. -Just one thing to do: We need to specify in the ``Poll`` model that ``Poll`` +Just one thing to do: We need to tell the admin that ``Poll`` objects have an admin interface. Edit the ``mysite/polls/models.py`` file and -make the following change to add an inner ``Admin`` class:: +add the following to the bottom of the file:: - class Poll(models.Model): - # ... - class Admin: - pass - -The ``class Admin`` will contain all the settings that control how this model -appears in the Django admin. All the settings are optional, however, so -creating an empty class means "give this object an admin interface using -all the default options." + from django.contrib import admin + + admin.site.register(Poll) Now reload the Django admin page to see your changes. Note that you don't have to restart the development server -- the server will auto-reload your project, @@ -92,8 +86,8 @@ so any modifications code will be seen immediately in your browser. Explore the free admin functionality ==================================== -Now that ``Poll`` has the inner ``Admin`` class, Django knows that it should be -displayed on the admin index page: +Now that we've registered ``Poll``, Django knows that it should be displayed on +the admin index page: .. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin03t.png :alt: Django admin index page, now with polls displayed @@ -145,17 +139,26 @@ with the timestamp and username of the person who made the change: Customize the admin form ======================== -Take a few minutes to marvel at all the code you didn't have to write. +Take a few minutes to marvel at all the code you didn't have to write. When you +call ``admin.site.register(Poll)``, Django just lets you edit the object and +"guess" at how to display it within the admin. Often you'll want to control how +the admin looks and works. You'll do this by telling Django about the options +you want when you register the object. -Let's customize this a bit. We can reorder the fields by explicitly adding a -``fields`` parameter to ``Admin``:: +Let's see how this works by reordering the fields on the edit form. Replace the +``admin.site.register(Poll)`` line with:: - class Admin: - fields = ( - (None, {'fields': ('pub_date', 'question')}), - ) + class PollAdmin(admin.ModelAdmin): + fields = ['pub_date', 'question'] + + admin.site.register(Poll, PollAdmin) -That made the "Publication date" show up first instead of second: +You'll follow this pattern -- create a model admin object, then pass it as the +second argument to ``admin.site.register()`` -- any time you need to change the +admin options for an object. + +This particular change above makes the "Publication date" come before the +"Question" field: .. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin07.png :alt: Fields have been reordered @@ -166,13 +169,15 @@ of fields, choosing an intuitive order is an important usability detail. And speaking of forms with dozens of fields, you might want to split the form up into fieldsets:: - class Admin: - fields = ( - (None, {'fields': ('question',)}), - ('Date information', {'fields': ('pub_date',)}), - ) + class PollAdmin(admin.ModelAdmin): + fieldsets = [ + (None, {'fields': ['question']}), + ('Date information', {'fields': ['pub_date']}), + ] + + admin.site.register(Poll, PollAdmin) -The first element of each tuple in ``fields`` is the title of the fieldset. +The first element of each tuple in ``fieldsets`` is the title of the fieldset. Here's what our form looks like now: .. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin08t.png @@ -184,11 +189,11 @@ You can assign arbitrary HTML classes to each fieldset. Django provides a This is useful when you have a long form that contains a number of fields that aren't commonly used:: - class Admin: - fields = ( - (None, {'fields': ('question',)}), - ('Date information', {'fields': ('pub_date',), 'classes': 'collapse'}), - ) + class PollAdmin(admin.ModelAdmin): + fieldsets = [ + (None, {'fields': ['question']}), + ('Date information', {'fields': ['pub_date'], 'classes': 'pub_date'}), + ] .. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin09.png :alt: Fieldset is initially collapsed @@ -201,14 +206,10 @@ the admin page doesn't display choices. Yet. -There are two ways to solve this problem. The first is to give the ``Choice`` -model its own inner ``Admin`` class, just as we did with ``Poll``. Here's what -that would look like:: +There are two ways to solve this problem. The first register ``Choice`` with the +admin just as we did with ``Poll``. That's easy:: - class Choice(models.Model): - # ... - class Admin: - pass + admin.site.register(Choice) Now "Choices" is an available option in the Django admin. The "Add choice" form looks like this: @@ -220,33 +221,35 @@ In that form, the "Poll" field is a select box containing every poll in the database. Django knows that a ``ForeignKey`` should be represented in the admin as a ``