Skip to content

Commit

Permalink
Fixed django#13023 - Removed ambiguity with regard to the max_num opt…
Browse files Browse the repository at this point in the history
…ion of formsets and as a result of admin inlines. Thanks to Gabriel Hurley for the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12872 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
jezdez committed Mar 27, 2010
1 parent 9df8d9c commit aba95dc
Show file tree
Hide file tree
Showing 13 changed files with 138 additions and 88 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -225,6 +225,7 @@ answer newbie questions, and generally made Django that much better:
John Huddleston <huddlej@wwu.edu>
Rob Hudson <http://rob.cogit8.org/>
Jason Huggins <http://www.jrandolph.com/blog/>
Gabriel Hurley <gabriel@strikeawe.com>
Hyun Mi Ae
Ibon <ibonso@gmail.com>
Tom Insam
Expand Down
9 changes: 5 additions & 4 deletions django/contrib/admin/media/js/inlines.js
Expand Up @@ -32,8 +32,9 @@
};
var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off");
var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off");
// only show the add button if we are allowed to add more items
var showAddButton = ((maxForms.val() == 0) || ((maxForms.val()-totalForms.val()) > 0));
// only show the add button if we are allowed to add more items,
// note that max_num = None translates to a blank string.
var showAddButton = maxForms.val() == '' || (maxForms.val()-totalForms.val()) > 0;
$(this).each(function(i) {
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
});
Expand Down Expand Up @@ -77,7 +78,7 @@
// Update number of total forms
$(totalForms).val(nextIndex + 1);
// Hide add button in case we've hit the max, except we want to add infinitely
if ((maxForms.val() != 0) && (maxForms.val() <= totalForms.val())) {
if ((maxForms.val() != '') && (maxForms.val() <= totalForms.val())) {
addButton.parent().hide();
}
// The delete button of each row triggers a bunch of other things
Expand All @@ -93,7 +94,7 @@
var forms = $("." + options.formCssClass);
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
// Show add button again once we drop below max
if ((maxForms.val() == 0) || (maxForms.val() >= forms.length)) {
if ((maxForms.val() == '') || (maxForms.val() >= forms.length)) {
addButton.parent().show();
}
// Also, update names and ids for all remaining form controls
Expand Down
6 changes: 3 additions & 3 deletions django/contrib/admin/media/js/inlines.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion django/contrib/admin/options.py
Expand Up @@ -1179,7 +1179,7 @@ class InlineModelAdmin(BaseModelAdmin):
fk_name = None
formset = BaseInlineFormSet
extra = 3
max_num = 0
max_num = None
template = None
verbose_name = None
verbose_name_plural = None
Expand Down
14 changes: 9 additions & 5 deletions django/contrib/admin/validation.py
Expand Up @@ -170,11 +170,15 @@ def validate_inline(cls, parent, parent_model):
fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True)

# extra = 3
# max_num = 0
for attr in ('extra', 'max_num'):
if not isinstance(getattr(cls, attr), int):
raise ImproperlyConfigured("'%s.%s' should be a integer."
% (cls.__name__, attr))
if not isinstance(getattr(cls, 'extra'), int):
raise ImproperlyConfigured("'%s.extra' should be a integer."
% cls.__name__)

# max_num = None
max_num = getattr(cls, 'max_num', None)
if max_num is not None and not isinstance(max_num, int):
raise ImproperlyConfigured("'%s.max_num' should be an integer or None (default)."
% cls.__name__)

# formset
if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet):
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/contenttypes/generic.py
Expand Up @@ -337,7 +337,7 @@ def generic_inlineformset_factory(model, form=ModelForm,
ct_field="content_type", fk_field="object_id",
fields=None, exclude=None,
extra=3, can_order=False, can_delete=True,
max_num=0,
max_num=None,
formfield_callback=lambda f: f.formfield()):
"""
Returns an ``GenericInlineFormSet`` for the given kwargs.
Expand Down
15 changes: 10 additions & 5 deletions django/forms/formsets.py
Expand Up @@ -25,7 +25,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_NUM_FORM_COUNT] = IntegerField(widget=HiddenInput)
self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput)
super(ManagementForm, self).__init__(*args, **kwargs)

class BaseFormSet(StrAndUnicode):
Expand Down Expand Up @@ -69,8 +69,13 @@ def total_form_count(self):
if self.data or self.files:
return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
else:
total_forms = self.initial_form_count() + self.extra
if total_forms > self.max_num > 0:
initial_forms = self.initial_form_count()
total_forms = initial_forms + self.extra
# Allow all existing related objects/inlines to be displayed,
# but don't allow extra beyond max_num.
if initial_forms > self.max_num >= 0:
total_forms = initial_forms
elif total_forms > self.max_num >= 0:
total_forms = self.max_num
return total_forms

Expand All @@ -81,7 +86,7 @@ def initial_form_count(self):
else:
# Use the length of the inital data if it's there, 0 otherwise.
initial_forms = self.initial and len(self.initial) or 0
if initial_forms > self.max_num > 0:
if initial_forms > self.max_num >= 0:
initial_forms = self.max_num
return initial_forms

Expand Down Expand Up @@ -324,7 +329,7 @@ def as_table(self):
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, max_num=0):
can_delete=False, max_num=None):
"""Return a FormSet for the given form class."""
attrs = {'form': form, 'extra': extra,
'can_order': can_order, 'can_delete': can_delete,
Expand Down
12 changes: 6 additions & 6 deletions django/forms/models.py
Expand Up @@ -448,10 +448,10 @@ def get_queryset(self):
if not qs.ordered:
qs = qs.order_by(self.model._meta.pk.name)

if self.max_num > 0:
self._queryset = qs[:self.max_num]
else:
self._queryset = qs
# Removed queryset limiting here. As per discussion re: #13023
# on django-dev, max_num should not prevent existing
# related objects/inlines from being displayed.
self._queryset = qs
return self._queryset

def save_new(self, form, commit=True):
Expand Down Expand Up @@ -649,7 +649,7 @@ def pk_is_not_editable(pk):
def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
formset=BaseModelFormSet,
extra=1, can_delete=False, can_order=False,
max_num=0, fields=None, exclude=None):
max_num=None, fields=None, exclude=None):
"""
Returns a FormSet class for the given Django model class.
"""
Expand Down Expand Up @@ -799,7 +799,7 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False):
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, max_num=0,
extra=3, can_order=False, can_delete=True, max_num=None,
formfield_callback=lambda f: f.formfield()):
"""
Returns an ``InlineFormSet`` for the given kwargs.
Expand Down
28 changes: 20 additions & 8 deletions docs/topics/forms/formsets.txt
Expand Up @@ -71,7 +71,7 @@ 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::
limit the maximum number of empty forms the formset will display::

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
>>> formset = ArticleFormset()
Expand All @@ -80,8 +80,20 @@ force the maximum number of forms the formset will display::
<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>

A ``max_num`` value of ``0`` (the default) puts no limit on the number forms
displayed.
.. versionchanged:: 1.2

If the value of ``max_num`` is geater than the number of existing related
objects, up to ``extra`` additional blank forms will be added to the formset,
so long as the total number of forms does not exceed ``max_num``.

A ``max_num`` value of ``None`` (the default) puts no limit on the number of
forms displayed. Please note that the default value of ``max_num`` was changed
from ``0`` to ``None`` in version 1.2 to allow ``0`` as a valid value.

.. versionadded:: 1.2

The dynamic "Add Another" link in the Django admin will not appear if
``max_num`` is less than the number of currently displayed forms.

Formset validation
------------------
Expand All @@ -102,7 +114,7 @@ provide an invalid article::
>>> data = {
... 'form-TOTAL_FORMS': u'2',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'16 June 1904',
... 'form-1-title': u'Test',
Expand Down Expand Up @@ -190,7 +202,7 @@ is where you define your own validation that works at the formset level::
>>> data = {
... 'form-TOTAL_FORMS': u'2',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'16 June 1904',
... 'form-1-title': u'Test',
Expand Down Expand Up @@ -249,7 +261,7 @@ happen when the user changes these values::
>>> data = {
... 'form-TOTAL_FORMS': u'3',
... 'form-INITIAL_FORMS': u'2',
... 'form-MAX_NUM_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Article #1',
... 'form-0-pub_date': u'2008-05-10',
... 'form-0-ORDER': u'2',
Expand Down Expand Up @@ -287,7 +299,7 @@ Lets create a formset with the ability to delete::
... ])
>>> for form in formset.forms:
.... print form.as_table()
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" value="0" id="id_form-MAX_NUM_FORMS" />
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" />
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" 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" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-DELETE">Delete:</label></th><td><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /></td></tr>
Expand All @@ -305,7 +317,7 @@ delete fields you can access them with ``deleted_forms``::
>>> data = {
... 'form-TOTAL_FORMS': u'3',
... 'form-INITIAL_FORMS': u'2',
... 'form-MAX_NUM_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Article #1',
... 'form-0-pub_date': u'2008-05-10',
... 'form-0-DELETE': u'on',
Expand Down
31 changes: 19 additions & 12 deletions docs/topics/forms/modelforms.txt
Expand Up @@ -369,7 +369,7 @@ Overriding the default field types or widgets
The default field types, as described in the `Field types`_ table above, are
sensible defaults. If you have a ``DateField`` in your model, chances are you'd
want that to be represented as a ``DateField`` in your form. But
``ModelForm`` gives you the flexibility of changing the form field type and
``ModelForm`` gives you the flexibility of changing the form field type and
widget for a given model field.

To specify a custom widget for a field, use the ``widgets`` attribute of the
Expand Down Expand Up @@ -401,7 +401,7 @@ field, you could do the following::

class ArticleForm(ModelForm):
pub_date = MyDateFormField()

class Meta:
model = Article

Expand Down Expand Up @@ -557,7 +557,7 @@ with the ``Author`` model. It works just like a regular formset::

>>> formset = AuthorFormSet()
>>> print formset
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" value="0" id="id_form-MAX_NUM_FORMS" />
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" />
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /></td></tr>
<tr><th><label for="id_form-0-title">Title:</label></th><td><select name="form-0-title" id="id_form-0-title">
<option value="" selected="selected">---------</option>
Expand Down Expand Up @@ -653,22 +653,24 @@ are saved properly.
Limiting the number of editable objects
---------------------------------------

.. versionchanged:: 1.2

As with regular formsets, you can use the ``max_num`` parameter to
``modelformset_factory`` to limit the number of forms displayed. With
model formsets, this property limits the query to select only the maximum
number of objects needed::
``modelformset_factory`` to limit the number of extra forms displayed.

``max_num`` does not prevent existing objects from being displayed::

>>> Author.objects.order_by('name')
[<Author: Charles Baudelaire>, <Author: Paul Verlaine>, <Author: Walt Whitman>]

>>> AuthorFormSet = modelformset_factory(Author, max_num=2, extra=1)
>>> AuthorFormSet = modelformset_factory(Author, max_num=1)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
>>> formset.initial
[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}]
>>> [x.name for x in formset.get_queryset()]
[u'Charles Baudelaire', u'Paul Verlaine', u'Walt Whitman']

If the value of ``max_num`` is higher than the number of objects returned, up to
``extra`` additional blank forms will be added to the formset, so long as the
total number of forms does not exceed ``max_num``::
If the value of ``max_num`` is geater than the number of existing related
objects, up to ``extra`` additional blank forms will be added to the formset,
so long as the total number of forms does not exceed ``max_num``::

>>> AuthorFormSet = modelformset_factory(Author, max_num=4, extra=2)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
Expand All @@ -679,6 +681,11 @@ total number of forms does not exceed ``max_num``::
<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>

.. versionchanged:: 1.2

A ``max_num`` value of ``None`` (the default) puts no limit on the number of
forms displayed.

Using a model formset in a view
-------------------------------

Expand Down

0 comments on commit aba95dc

Please sign in to comment.