1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

newforms-admin: Fixed #6075 -- Implemented max_num on formsets and model formsets. Added a hook on InlineModelAdmin to customize in the admin interface.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7613 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Brian Rosner 2008-06-11 03:58:01 +00:00
parent edf396da59
commit 530670e27f
7 changed files with 170 additions and 26 deletions

View File

@ -699,6 +699,7 @@ class InlineModelAdmin(BaseModelAdmin):
fk_name = None
formset = BaseInlineFormset
extra = 3
max_num = 0
template = None
verbose_name = None
verbose_name_plural = None
@ -722,7 +723,7 @@ class InlineModelAdmin(BaseModelAdmin):
return inlineformset_factory(self.parent_model, self.model,
form=self.form, formset=self.formset, fk_name=self.fk_name,
fields=fields, formfield_callback=self.formfield_for_dbfield,
extra=self.extra)
extra=self.extra, max_num=self.max_num)
def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets:

View File

@ -10,6 +10,7 @@ __all__ = ('BaseFormSet', 'all_valid')
# special field names
TOTAL_FORM_COUNT = 'TOTAL_FORMS'
INITIAL_FORM_COUNT = 'INITIAL_FORMS'
MAX_FORM_COUNT = 'MAX_FORMS'
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'
@ -22,6 +23,7 @@ class ManagementForm(Form):
def __init__(self, *args, **kwargs):
self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
self.base_fields[MAX_FORM_COUNT] = IntegerField(widget=HiddenInput)
super(ManagementForm, self).__init__(*args, **kwargs)
class BaseFormSet(StrAndUnicode):
@ -29,7 +31,7 @@ class BaseFormSet(StrAndUnicode):
A collection of instances of the same Form class.
"""
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList):
initial=None, error_class=ErrorList):
self.is_bound = data is not None or files is not None
self.prefix = prefix or 'form'
self.auto_id = auto_id
@ -45,18 +47,25 @@ class BaseFormSet(StrAndUnicode):
if self.management_form.is_valid():
self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
self._max_form_count = self.management_form.cleaned_data[MAX_FORM_COUNT]
else:
raise ValidationError('ManagementForm data is missing or has been tampered with')
else:
if initial:
self._initial_form_count = len(initial)
if self._initial_form_count > self._max_form_count and self._max_form_count > 0:
self._initial_form_count = self._max_form_count
self._total_form_count = self._initial_form_count + self.extra
else:
self._initial_form_count = 0
self._total_form_count = self.extra
initial = {TOTAL_FORM_COUNT: self._total_form_count, INITIAL_FORM_COUNT: self._initial_form_count}
if self._total_form_count > self._max_form_count and self._max_form_count > 0:
self._total_form_count = self._max_form_count
initial = {TOTAL_FORM_COUNT: self._total_form_count,
INITIAL_FORM_COUNT: self._initial_form_count,
MAX_FORM_COUNT: self._max_form_count}
self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
# construct the forms in the formset
self._construct_forms()
@ -266,9 +275,11 @@ class BaseFormSet(StrAndUnicode):
return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
can_delete=False):
can_delete=False, max_num=0):
"""Return a FormSet for the given form class."""
attrs = {'form': form, 'extra': extra, 'can_order': can_order, 'can_delete': can_delete}
attrs = {'form': form, 'extra': extra,
'can_order': can_order, 'can_delete': can_delete,
'_max_form_count': max_num}
return type(form.__name__ + 'FormSet', (formset,), attrs)
def all_valid(formsets):

View File

@ -305,7 +305,11 @@ class BaseModelFormSet(BaseFormSet):
queryset=None, **kwargs):
self.queryset = queryset
defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
defaults['initial'] = [model_to_dict(obj) for obj in self.get_queryset()]
if self._max_form_count > 0:
qs = self.get_queryset()[:self._max_form_count]
else:
qs = self.get_queryset()
defaults['initial'] = [model_to_dict(obj) for obj in qs]
defaults.update(kwargs)
super(BaseModelFormSet, self).__init__(**defaults)
@ -369,15 +373,16 @@ class BaseModelFormSet(BaseFormSet):
super(BaseModelFormSet, self).add_fields(form, index)
def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
formset=BaseModelFormSet,
extra=1, can_delete=False, can_order=False,
fields=None, exclude=None):
formset=BaseModelFormSet,
extra=1, can_delete=False, can_order=False,
max_num=0, fields=None, exclude=None):
"""
Returns a FormSet class for the given Django model class.
"""
form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
formfield_callback=formfield_callback)
FormSet = formset_factory(form, formset, extra=extra, can_order=can_order, can_delete=can_delete)
formfield_callback=formfield_callback)
FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
can_order=can_order, can_delete=can_delete)
FormSet.model = model
return FormSet
@ -395,9 +400,8 @@ class BaseInlineFormset(BaseModelFormSet):
super(BaseInlineFormset, self).__init__(data, files, prefix=self.rel_name)
def _construct_forms(self):
from django.newforms.formsets import INITIAL_FORM_COUNT
if self.save_as_new:
self._total_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
self._total_form_count = self._initial_form_count
self._initial_form_count = 0
super(BaseInlineFormset, self)._construct_forms()
@ -443,10 +447,10 @@ def _get_foreign_key(parent_model, model, fk_name=None):
def inlineformset_factory(parent_model, model, form=ModelForm,
formset=BaseInlineFormset, fk_name=None,
fields=None, exclude=None,
extra=3, can_order=False, can_delete=True,
formfield_callback=lambda f: f.formfield()):
formset=BaseInlineFormset, fk_name=None,
fields=None, exclude=None,
extra=3, can_order=False, can_delete=True, max_num=0,
formfield_callback=lambda f: f.formfield()):
"""
Returns an ``InlineFormset`` for the given kwargs.
@ -464,7 +468,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
formfield_callback=formfield_callback,
formset=formset,
extra=extra, can_delete=can_delete, can_order=can_order,
fields=fields, exclude=exclude)
fields=fields, exclude=exclude, max_num=max_num)
FormSet.fk = fk
return FormSet

View File

@ -449,6 +449,34 @@ model instances without any database interaction::
This gives you the ability to attach data to the instances before saving them
to the database.
Limiting the number of objects editable
---------------------------------------
Similar to regular formsets you can use the ``max_num`` parameter to
``modelformset_factory`` to limit the number of forms displayed. With
model formsets this will properly limit the query to only select the maximum
number of objects needed::
>>> Author.objects.order_by('name')
[<Author: Charles Baudelaire>, <Author: Paul Verlaine>, <Author: Walt Whitman>]
>>> AuthorFormSet = modelformset_factory(Author, max_num=2, extra=1)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
>>> formset.initial
[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}]
If the value of ``max_num`` is less than the total objects returned it will
fill the rest with extra forms::
>>> AuthorFormSet = modelformset_factory(Author, max_num=4, extra=1)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
>>> for form in formset.forms:
... print form.as_table()
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-0-id" value="1" id="id_form-0-id" /></td></tr>
<tr><th><label for="id_form-1-name">Name:</label></th><td><input id="id_form-1-name" type="text" name="form-1-name" value="Paul Verlaine" maxlength="100" /><input type="hidden" name="form-1-id" value="3" id="id_form-1-id" /></td></tr>
<tr><th><label for="id_form-2-name">Name:</label></th><td><input id="id_form-2-name" type="text" name="form-2-name" value="Walt Whitman" maxlength="100" /><input type="hidden" name="form-2-id" value="2" id="id_form-2-id" /></td></tr>
<tr><th><label for="id_form-3-name">Name:</label></th><td><input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></td></tr>
Using ``inlineformset_factory``
-------------------------------

View File

@ -2230,6 +2230,22 @@ 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()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
The default value of ``max_num`` is ``0`` which is the same as saying put no
limit on the number forms displayed.
Formset validation
------------------

View File

@ -31,6 +31,7 @@ __test__ = {'API_TESTS': """
>>> data = {
... 'form-TOTAL_FORMS': '3', # the number of forms rendered
... 'form-INITIAL_FORMS': '0', # the number of forms with initial data
... 'form-MAX_FORMS': '0', # the max number of forms
... 'form-0-name': 'Charles Baudelaire',
... 'form-1-name': 'Arthur Rimbaud',
... 'form-2-name': '',
@ -68,6 +69,7 @@ them in alphabetical order by name.
>>> data = {
... 'form-TOTAL_FORMS': '3', # the number of forms rendered
... 'form-INITIAL_FORMS': '2', # the number of forms with initial data
... 'form-MAX_FORMS': '0', # the max number of forms
... 'form-0-id': '2',
... 'form-0-name': 'Arthur Rimbaud',
... 'form-1-id': '1',
@ -111,6 +113,7 @@ deltetion, make sure we don't save that form.
>>> data = {
... 'form-TOTAL_FORMS': '4', # the number of forms rendered
... 'form-INITIAL_FORMS': '3', # the number of forms with initial data
... 'form-MAX_FORMS': '0', # the max number of forms
... 'form-0-id': '2',
... 'form-0-name': 'Arthur Rimbaud',
... 'form-1-id': '1',
@ -140,6 +143,7 @@ Let's edit a record to ensure save only returns that one record.
>>> data = {
... 'form-TOTAL_FORMS': '4', # the number of forms rendered
... 'form-INITIAL_FORMS': '3', # the number of forms with initial data
... 'form-MAX_FORMS': '0', # the max number of forms
... 'form-0-id': '2',
... 'form-0-name': 'Walt Whitman',
... 'form-1-id': '1',
@ -158,6 +162,22 @@ True
>>> formset.save()
[<Author: Walt Whitman>]
Test the behavior of max_num with model formsets. It should properly limit
the queryset to reduce the amount of objects being pulled in when not being
used.
>>> qs = Author.objects.order_by('name')
>>> AuthorFormSet = modelformset_factory(Author, max_num=2)
>>> formset = AuthorFormSet(queryset=qs)
>>> formset.initial
[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}]
>>> AuthorFormSet = modelformset_factory(Author, max_num=3)
>>> formset = AuthorFormSet(queryset=qs)
>>> formset.initial
[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}, {'id': 2, 'name': u'Walt Whitman'}]
# Inline Formsets ############################################################
We can also create a formset that is tied to a parent model. This is how the
@ -178,6 +198,7 @@ admin system's edit inline functionality works.
>>> data = {
... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
... 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data
... 'book_set-MAX_FORMS': '0', # the max number of forms
... 'book_set-0-title': 'Les Fleurs du Mal',
... 'book_set-1-title': '',
... 'book_set-2-title': '',
@ -212,6 +233,7 @@ book.
>>> data = {
... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data
... 'book_set-MAX_FORMS': '0', # the max number of forms
... 'book_set-0-id': '1',
... 'book_set-0-title': 'Les Fleurs du Mal',
... 'book_set-1-title': 'Le Spleen de Paris',
@ -238,6 +260,7 @@ This is used in the admin for save_as functionality.
>>> data = {
... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
... 'book_set-INITIAL_FORMS': '2', # the number of forms with initial data
... 'book_set-MAX_FORMS': '0', # the max number of forms
... 'book_set-0-id': '1',
... 'book_set-0-title': 'Les Fleurs du Mal',
... 'book_set-1-id': '2',

View File

@ -20,7 +20,7 @@ but we'll look at how to do so later.
>>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
>>> print formset
<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" />
<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_FORMS" value="0" />
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>
@ -34,6 +34,7 @@ the TOTAL_FORMS field appropriately.
>>> data = {
... 'choices-TOTAL_FORMS': '1', # the number of forms rendered
... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... }
@ -60,6 +61,7 @@ any of the forms.
>>> data = {
... 'choices-TOTAL_FORMS': '1', # the number of forms rendered
... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '',
... }
@ -90,6 +92,7 @@ Let's simulate what would happen if we submitted this form.
>>> data = {
... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': '',
@ -111,6 +114,7 @@ handle that later.
>>> data = {
... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': 'The Decemberists',
@ -130,6 +134,7 @@ handle that case later.
>>> data = {
... 'choices-TOTAL_FORMS': '2', # the number of forms rendered
... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': '', # deleted value
... 'choices-0-votes': '', # deleted value
... 'choices-1-choice': '',
@ -167,6 +172,7 @@ number of forms to be completed.
>>> data = {
... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': '',
... 'choices-0-votes': '',
... 'choices-1-choice': '',
@ -187,6 +193,7 @@ We can just fill out one of the forms.
>>> data = {
... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': '',
@ -207,6 +214,7 @@ And once again, if we try to partially complete a form, validation will fail.
>>> data = {
... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-1-choice': 'The Decemberists',
@ -267,6 +275,7 @@ To delete something, we just need to set that form's special delete field to
>>> data = {
... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-DELETE': '',
@ -316,6 +325,7 @@ something at the front of the list, you'd need to set it's order to 0.
>>> data = {
... 'choices-TOTAL_FORMS': '3', # the number of forms rendered
... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1',
@ -342,6 +352,7 @@ they will be sorted below everything else.
>>> data = {
... 'choices-TOTAL_FORMS': '4', # the number of forms rendered
... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1',
@ -403,6 +414,7 @@ Let's delete Fergie, and put The Decemberists ahead of Calexico.
>>> data = {
... 'choices-TOTAL_FORMS': '4', # the number of forms rendered
... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
... 'choices-MAX_FORMS': '0', # the max number of forms
... 'choices-0-choice': 'Calexico',
... 'choices-0-votes': '100',
... 'choices-0-ORDER': '1',
@ -444,12 +456,7 @@ error if there are any duplicates.
... name = CharField()
...
>>> class FavoriteDrinksFormSet(BaseFormSet):
... form = FavoriteDrinkForm
... extra = 2
... can_order = False
... can_delete = False
...
>>> class BaseFavoriteDrinksFormSet(BaseFormSet):
... def clean(self):
... seen_drinks = []
... for drink in self.cleaned_data:
@ -458,11 +465,15 @@ error if there are any duplicates.
... seen_drinks.append(drink['name'])
...
>>> FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm,
... formset=BaseFavoriteDrinksFormSet, extra=3)
We start out with a some duplicate data.
>>> data = {
... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
... 'drinks-MAX_FORMS': '0', # the max number of forms
... 'drinks-0-name': 'Gin and Tonic',
... 'drinks-1-name': 'Gin and Tonic',
... }
@ -484,6 +495,7 @@ Make sure we didn't break the valid case.
>>> data = {
... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
... 'drinks-MAX_FORMS': '0', # the max number of forms
... 'drinks-0-name': 'Gin and Tonic',
... 'drinks-1-name': 'Bloody Mary',
... }
@ -494,6 +506,55 @@ True
>>> for error in formset.non_form_errors():
... print error
# Limiting the maximum number of forms ########################################
# Base case for max_num.
>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2)
>>> formset = LimitedFavoriteDrinkFormSet()
>>> for form in formset.forms:
... print form
<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr>
<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>
# Ensure the that max_num has no affect when extra is less than max_forms.
>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2)
>>> formset = LimitedFavoriteDrinkFormSet()
>>> for form in formset.forms:
... print form
<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr>
# max_num with initial data
# More initial forms than max_num will result in only the first max_num of
# them to be displayed with no extra forms.
>>> initial = [
... {'name': 'Gin Tonic'},
... {'name': 'Bloody Mary'},
... {'name': 'Jack and Coke'},
... ]
>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2)
>>> formset = LimitedFavoriteDrinkFormSet(initial=initial)
>>> for form in formset.forms:
... print form
<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr>
<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" value="Bloody Mary" id="id_form-1-name" /></td></tr>
# One form from initial and extra=3 with max_num=2 should result in the one
# initial form and one extra.
>>> initial = [
... {'name': 'Gin Tonic'},
... ]
>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=2)
>>> formset = LimitedFavoriteDrinkFormSet(initial=initial)
>>> for form in formset.forms:
... print form
<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr>
<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>
# Regression test for #6926 ##################################################