Author: jezdez
Date: 2010-03-27 18:03:56 -0500 (Sat, 27 Mar 2010)
New Revision: 12872

Modified:
   django/trunk/AUTHORS
   django/trunk/django/contrib/admin/media/js/inlines.js
   django/trunk/django/contrib/admin/media/js/inlines.min.js
   django/trunk/django/contrib/admin/options.py
   django/trunk/django/contrib/admin/validation.py
   django/trunk/django/contrib/contenttypes/generic.py
   django/trunk/django/forms/formsets.py
   django/trunk/django/forms/models.py
   django/trunk/docs/topics/forms/formsets.txt
   django/trunk/docs/topics/forms/modelforms.txt
   django/trunk/tests/modeltests/model_formsets/models.py
   django/trunk/tests/regressiontests/forms/formsets.py
   django/trunk/tests/regressiontests/modeladmin/models.py
Log:
Fixed #13023 - Removed ambiguity with regard to the max_num option of formsets 
and as a result of admin inlines. Thanks to Gabriel Hurley for the patch.

Modified: django/trunk/AUTHORS
===================================================================
--- django/trunk/AUTHORS        2010-03-27 23:03:12 UTC (rev 12871)
+++ django/trunk/AUTHORS        2010-03-27 23:03:56 UTC (rev 12872)
@@ -225,6 +225,7 @@
     John Huddleston <[email protected]>
     Rob Hudson <http://rob.cogit8.org/>
     Jason Huggins <http://www.jrandolph.com/blog/>
+    Gabriel Hurley <[email protected]>
     Hyun Mi Ae
     Ibon <[email protected]>
     Tom Insam

Modified: django/trunk/django/contrib/admin/media/js/inlines.js
===================================================================
--- django/trunk/django/contrib/admin/media/js/inlines.js       2010-03-27 
23:03:12 UTC (rev 12871)
+++ django/trunk/django/contrib/admin/media/js/inlines.js       2010-03-27 
23:03:56 UTC (rev 12872)
@@ -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);
                });
@@ -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
@@ -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

Modified: django/trunk/django/contrib/admin/media/js/inlines.min.js
===================================================================
--- django/trunk/django/contrib/admin/media/js/inlines.min.js   2010-03-27 
23:03:12 UTC (rev 12871)
+++ django/trunk/django/contrib/admin/media/js/inlines.min.js   2010-03-27 
23:03:56 UTC (rev 12872)
@@ -1,5 +1,5 @@
-(function(a){a.fn.formset=function(f){var 
b=a.extend({},a.fn.formset.defaults,f),l=function(d,e,j){var c=new 
RegExp("("+e+"-\\d+)");e=e+"-"+j;a(d).attr("for")&&a(d).attr("for",a(d).attr("for").replace(c,e));if(d.id)d.id=d.id.replace(c,e);if(d.name)d.name=d.name.replace(c,e)};f=a("#id_"+b.prefix+"-TOTAL_FORMS").attr("autocomplete","off");var
 
h=a("#id_"+b.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off");f=h.val()==0||h.val()-f.val()>0;a(this).each(function(){a(this).not("."+b.emptyCssClass).addClass(b.formCssClass)});
+(function(a){a.fn.formset=function(f){var 
b=a.extend({},a.fn.formset.defaults,f),l=function(d,e,j){var c=new 
RegExp("("+e+"-\\d+)");e=e+"-"+j;a(d).attr("for")&&a(d).attr("for",a(d).attr("for").replace(c,e));if(d.id)d.id=d.id.replace(c,e);if(d.name)d.name=d.name.replace(c,e)};f=a("#id_"+b.prefix+"-TOTAL_FORMS").attr("autocomplete","off");var
 
h=a("#id_"+b.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off");f=h.val()==""||h.val()-f.val()>0;a(this).each(function(){a(this).not("."+b.emptyCssClass).addClass(b.formCssClass)});
 if(a(this).length&&f){var 
i;if(a(this).attr("tagName")=="TR"){f=this.eq(0).children().length;a(this).parent().append('<tr
 class="'+b.addCssClass+'"><td colspan="'+f+'"><a 
href="javascript:void(0)">'+b.addText+"</a></tr>");i=a(this).parent().find("tr:last
 a")}else{a(this).filter(":last").after('<div class="'+b.addCssClass+'"><a 
href="javascript:void(0)">'+b.addText+"</a></div>");i=a(this).filter(":last").next().find("a")}i.click(function(){var
 d=a("#id_"+b.prefix+"-TOTAL_FORMS"),e=parseInt(d.val()),
-j=a("#"+b.prefix+"-empty"),c=j.clone(true).get(0);a(c).removeClass(b.emptyCssClass).removeAttr("id").insertBefore(a(j));a(c).html(a(c).html().replace(/__prefix__/g,e));a(c).addClass(b.formCssClass).attr("id",b.prefix+(parseInt(e)+1));if(a(c).is("TR"))a(c).children(":last").append('<div><a
 class="'+b.deleteCssClass+'" 
href="javascript:void(0)">'+b.deleteText+"</a></div>");else 
a(c).is("UL")||a(c).is("OL")?a(c).append('<li><a class="'+b.deleteCssClass+'" 
href="javascript:void(0)">'+b.deleteText+"</a></li>"):
-a(c).children(":first").append('<span><a class="'+b.deleteCssClass+'" 
href="javascript:void(0)">'+b.deleteText+"</a></span>");a(c).find("input,select,textarea,label,a").each(function(){l(this,b.prefix,d.val())});a(d).val(e+1);h.val()!=0&&h.val()<=d.val()&&i.parent().hide();a(c).find("a."+b.deleteCssClass).click(function(){var
 
g=a(this).parents("."+b.formCssClass);g.remove();b.removed&&b.removed(g);g=a("."+b.formCssClass);a("#id_"+b.prefix+"-TOTAL_FORMS").val(g.length);if(h.val()==0||h.val()>=g.length)i.parent().show();
+j=a("#"+b.prefix+"-empty"),c=j.clone(true).get(0);a(c).removeClass(b.emptyCssClass).removeAttr("id").insertBefore(a(j));a(c).html(a(c).html().replace(/__prefix__/g,e));a(c).addClass(b.formCssClass).attr("id",b.prefix+(e+1));if(a(c).is("TR"))a(c).children(":last").append('<div><a
 class="'+b.deleteCssClass+'" 
href="javascript:void(0)">'+b.deleteText+"</a></div>");else 
a(c).is("UL")||a(c).is("OL")?a(c).append('<li><a class="'+b.deleteCssClass+'" 
href="javascript:void(0)">'+b.deleteText+"</a></li>"):
+a(c).children(":first").append('<span><a class="'+b.deleteCssClass+'" 
href="javascript:void(0)">'+b.deleteText+"</a></span>");a(c).find("input,select,textarea,label,a").each(function(){l(this,b.prefix,d.val())});a(d).val(e+1);h.val()!=""&&h.val()<=d.val()&&i.parent().hide();a(c).find("a."+b.deleteCssClass).click(function(){var
 
g=a(this).parents("."+b.formCssClass);g.remove();b.removed&&b.removed(g);g=a("."+b.formCssClass);a("#id_"+b.prefix+"-TOTAL_FORMS").val(g.length);if(h.val()==""||h.val()>=g.length)i.parent().show();
 for(var 
k=0,m=g.length;k<m;k++)a(g.get(k)).find("input,select,textarea,label,a").each(function(){l(this,b.prefix,k)});return
 false});b.added&&b.added(a(c));return false})}return 
this};a.fn.formset.defaults={prefix:"form",addText:"add 
another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null}})(jQuery);

Modified: django/trunk/django/contrib/admin/options.py
===================================================================
--- django/trunk/django/contrib/admin/options.py        2010-03-27 23:03:12 UTC 
(rev 12871)
+++ django/trunk/django/contrib/admin/options.py        2010-03-27 23:03:56 UTC 
(rev 12872)
@@ -1179,7 +1179,7 @@
     fk_name = None
     formset = BaseInlineFormSet
     extra = 3
-    max_num = 0
+    max_num = None
     template = None
     verbose_name = None
     verbose_name_plural = None

Modified: django/trunk/django/contrib/admin/validation.py
===================================================================
--- django/trunk/django/contrib/admin/validation.py     2010-03-27 23:03:12 UTC 
(rev 12871)
+++ django/trunk/django/contrib/admin/validation.py     2010-03-27 23:03:56 UTC 
(rev 12872)
@@ -170,12 +170,16 @@
     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):
         raise ImproperlyConfigured("'%s.formset' does not inherit from "

Modified: django/trunk/django/contrib/contenttypes/generic.py
===================================================================
--- django/trunk/django/contrib/contenttypes/generic.py 2010-03-27 23:03:12 UTC 
(rev 12871)
+++ django/trunk/django/contrib/contenttypes/generic.py 2010-03-27 23:03:56 UTC 
(rev 12872)
@@ -337,7 +337,7 @@
                                   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.

Modified: django/trunk/django/forms/formsets.py
===================================================================
--- django/trunk/django/forms/formsets.py       2010-03-27 23:03:12 UTC (rev 
12871)
+++ django/trunk/django/forms/formsets.py       2010-03-27 23:03:56 UTC (rev 
12872)
@@ -25,7 +25,7 @@
     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):
@@ -69,8 +69,13 @@
         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
 
@@ -81,7 +86,7 @@
         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
 
@@ -324,7 +329,7 @@
         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,

Modified: django/trunk/django/forms/models.py
===================================================================
--- django/trunk/django/forms/models.py 2010-03-27 23:03:12 UTC (rev 12871)
+++ django/trunk/django/forms/models.py 2010-03-27 23:03:56 UTC (rev 12872)
@@ -448,10 +448,10 @@
             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):
@@ -649,7 +649,7 @@
 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.
     """
@@ -799,7 +799,7 @@
 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.

Modified: django/trunk/docs/topics/forms/formsets.txt
===================================================================
--- django/trunk/docs/topics/forms/formsets.txt 2010-03-27 23:03:12 UTC (rev 
12871)
+++ django/trunk/docs/topics/forms/formsets.txt 2010-03-27 23:03:56 UTC (rev 
12872)
@@ -71,7 +71,7 @@
 ------------------------------------
 
 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()
@@ -80,9 +80,21 @@
     <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
 ------------------
 
@@ -102,7 +114,7 @@
     >>> 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',
@@ -190,7 +202,7 @@
     >>> 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',
@@ -249,7 +261,7 @@
     >>> 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',
@@ -287,7 +299,7 @@
     ... ])
     >>> 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>
@@ -305,7 +317,7 @@
     >>> 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',

Modified: django/trunk/docs/topics/forms/modelforms.txt
===================================================================
--- django/trunk/docs/topics/forms/modelforms.txt       2010-03-27 23:03:12 UTC 
(rev 12871)
+++ django/trunk/docs/topics/forms/modelforms.txt       2010-03-27 23:03:56 UTC 
(rev 12872)
@@ -369,7 +369,7 @@
 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
@@ -401,7 +401,7 @@
 
     class ArticleForm(ModelForm):
         pub_date = MyDateFormField()
-    
+
         class Meta:
             model = Article
 
@@ -557,7 +557,7 @@
 
     >>> 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>
@@ -653,22 +653,24 @@
 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'))
@@ -679,6 +681,11 @@
     <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
 -------------------------------
 

Modified: django/trunk/tests/modeltests/model_formsets/models.py
===================================================================
--- django/trunk/tests/modeltests/model_formsets/models.py      2010-03-27 
23:03:12 UTC (rev 12871)
+++ django/trunk/tests/modeltests/model_formsets/models.py      2010-03-27 
23:03:56 UTC (rev 12872)
@@ -200,7 +200,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '3', # the number of forms rendered
 ...     'form-INITIAL_FORMS': '0', # the number of forms with initial data
-...     'form-MAX_NUM_FORMS': '0', # the max number of forms
+...     'form-MAX_NUM_FORMS': '', # the max number of forms
 ...     'form-0-name': 'Charles Baudelaire',
 ...     'form-1-name': 'Arthur Rimbaud',
 ...     'form-2-name': '',
@@ -238,7 +238,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '3', # the number of forms rendered
 ...     'form-INITIAL_FORMS': '2', # the number of forms with initial data
-...     'form-MAX_NUM_FORMS': '0', # the max number of forms
+...     'form-MAX_NUM_FORMS': '', # the max number of forms
 ...     'form-0-id': '2',
 ...     'form-0-name': 'Arthur Rimbaud',
 ...     'form-1-id': '1',
@@ -282,7 +282,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '4', # the number of forms rendered
 ...     'form-INITIAL_FORMS': '3', # the number of forms with initial data
-...     'form-MAX_NUM_FORMS': '0', # the max number of forms
+...     'form-MAX_NUM_FORMS': '', # the max number of forms
 ...     'form-0-id': '2',
 ...     'form-0-name': 'Arthur Rimbaud',
 ...     'form-1-id': '1',
@@ -312,7 +312,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '4', # the number of forms rendered
 ...     'form-INITIAL_FORMS': '3', # the number of forms with initial data
-...     'form-MAX_NUM_FORMS': '0', # the max number of forms
+...     'form-MAX_NUM_FORMS': '', # the max number of forms
 ...     'form-0-id': '2',
 ...     'form-0-name': 'Walt Whitman',
 ...     'form-1-id': '1',
@@ -343,7 +343,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '2', # the number of forms rendered
 ...     'form-INITIAL_FORMS': '1', # the number of forms with initial data
-...     'form-MAX_NUM_FORMS': '0', # the max number of forms
+...     'form-MAX_NUM_FORMS': '', # the max number of forms
 ...     'form-0-id': '1',
 ...     'form-0-name': '2nd Tuesday of the Week Meeting',
 ...     'form-0-authors': [2, 1, 3, 4],
@@ -365,23 +365,43 @@
 # delete the author we created to allow later tests to continue working.
 >>> new_author.delete()
 
-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.
+Test the behavior of max_num with model formsets. It should allow all existing
+related objects/inlines for a given object to be displayed, but not allow
+the creation of new inlines beyond max_num.
 
 >>> qs = Author.objects.order_by('name')
 
->>> AuthorFormSet = modelformset_factory(Author, max_num=2)
+>>> AuthorFormSet = modelformset_factory(Author, max_num=None, extra=3)
 >>> formset = AuthorFormSet(queryset=qs)
+>>> len(formset.extra_forms)
+3
+
+>>> AuthorFormSet = modelformset_factory(Author, max_num=4, extra=3)
+>>> formset = AuthorFormSet(queryset=qs)
+>>> len(formset.extra_forms)
+1
+
+>>> AuthorFormSet = modelformset_factory(Author, max_num=0, extra=3)
+>>> formset = AuthorFormSet(queryset=qs)
+>>> len(formset.extra_forms)
+0
+
+>>> AuthorFormSet = modelformset_factory(Author, max_num=None)
+>>> formset = AuthorFormSet(queryset=qs)
 >>> [x.name for x in formset.get_queryset()]
-[u'Charles Baudelaire', u'Paul Verlaine']
+[u'Charles Baudelaire', u'Paul Verlaine', u'Walt Whitman']
 
->>> AuthorFormSet = modelformset_factory(Author, max_num=3)
+>>> AuthorFormSet = modelformset_factory(Author, max_num=0)
 >>> formset = AuthorFormSet(queryset=qs)
 >>> [x.name for x in formset.get_queryset()]
 [u'Charles Baudelaire', u'Paul Verlaine', u'Walt Whitman']
 
+>>> AuthorFormSet = modelformset_factory(Author, max_num=4)
+>>> formset = AuthorFormSet(queryset=qs)
+>>> [x.name for x in formset.get_queryset()]
+[u'Charles Baudelaire', u'Paul Verlaine', u'Walt Whitman']
 
+
 # ModelForm with a custom save method in a formset ###########################
 
 >>> class PoetForm(forms.ModelForm):
@@ -398,7 +418,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '3', # the number of forms rendered
 ...     'form-INITIAL_FORMS': '0', # the number of forms with initial data
-...     'form-MAX_NUM_FORMS': '0', # the max number of forms
+...     'form-MAX_NUM_FORMS': '', # the max number of forms
 ...     'form-0-name': 'Walt Whitman',
 ...     'form-1-name': 'Charles Baudelaire',
 ...     'form-2-name': '',
@@ -425,7 +445,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '1', # the number of forms rendered
 ...     'form-INITIAL_FORMS': '0', # the number of forms with initial data
-...     'form-MAX_NUM_FORMS': '0', # the max number of forms
+...     'form-MAX_NUM_FORMS': '', # the max number of forms
 ...     'form-0-author_ptr': '',
 ...     'form-0-name': 'Ernest Hemingway',
 ...     'form-0-write_speed': '10',
@@ -449,7 +469,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '2', # the number of forms rendered
 ...     'form-INITIAL_FORMS': '1', # the number of forms with initial data
-...     'form-MAX_NUM_FORMS': '0', # the max number of forms
+...     'form-MAX_NUM_FORMS': '', # the max number of forms
 ...     'form-0-author_ptr': hemingway_id,
 ...     'form-0-name': 'Ernest Hemingway',
 ...     'form-0-write_speed': '10',
@@ -484,7 +504,7 @@
 >>> 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_NUM_FORMS': '0', # the max number of forms
+...     'book_set-MAX_NUM_FORMS': '', # the max number of forms
 ...     'book_set-0-title': 'Les Fleurs du Mal',
 ...     'book_set-1-title': '',
 ...     'book_set-2-title': '',
@@ -519,7 +539,7 @@
 >>> 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_NUM_FORMS': '0', # the max number of forms
+...     'book_set-MAX_NUM_FORMS': '', # the max number of forms
 ...     'book_set-0-id': '1',
 ...     'book_set-0-title': 'Les Fleurs du Mal',
 ...     'book_set-1-title': 'Les Paradis Artificiels',
@@ -546,7 +566,7 @@
 >>> 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_NUM_FORMS': '0', # the max number of forms
+...     'book_set-MAX_NUM_FORMS': '', # the max number of forms
 ...     'book_set-0-id': '1',
 ...     'book_set-0-title': 'Les Fleurs du Mal',
 ...     'book_set-1-id': '2',
@@ -584,7 +604,7 @@
 >>> data = {
 ...     'bookwithcustompk_set-TOTAL_FORMS': '1', # the number of forms rendered
 ...     'bookwithcustompk_set-INITIAL_FORMS': '0', # the number of forms with 
initial data
-...     'bookwithcustompk_set-MAX_NUM_FORMS': '0', # the max number of forms
+...     'bookwithcustompk_set-MAX_NUM_FORMS': '', # the max number of forms
 ...     'bookwithcustompk_set-0-my_pk': '77777',
 ...     'bookwithcustompk_set-0-title': 'Les Fleurs du Mal',
 ... }
@@ -615,7 +635,7 @@
 >>> data = {
 ...     'alternatebook_set-TOTAL_FORMS': '1', # the number of forms rendered
 ...     'alternatebook_set-INITIAL_FORMS': '0', # the number of forms with 
initial data
-...     'alternatebook_set-MAX_NUM_FORMS': '0', # the max number of forms
+...     'alternatebook_set-MAX_NUM_FORMS': '', # the max number of forms
 ...     'alternatebook_set-0-title': 'Flowers of Evil',
 ...     'alternatebook_set-0-notes': 'English translation of Les Fleurs du Mal'
 ... }
@@ -644,7 +664,7 @@
 >>> data = {
 ...     'poem_set-TOTAL_FORMS': '3', # the number of forms rendered
 ...     'poem_set-INITIAL_FORMS': '0', # the number of forms with initial data
-...     'poem_set-MAX_NUM_FORMS': '0', # the max number of forms
+...     'poem_set-MAX_NUM_FORMS': '', # the max number of forms
 ...     'poem_set-0-name': 'The Cloud in Trousers',
 ...     'poem_set-1-name': 'I',
 ...     'poem_set-2-name': '',
@@ -673,7 +693,7 @@
 >>> data = {
 ...     'book_set-TOTAL_FORMS': '5', # the number of forms rendered
 ...     'book_set-INITIAL_FORMS': '3', # the number of forms with initial data
-...     'book_set-MAX_NUM_FORMS': '0', # the max number of forms
+...     'book_set-MAX_NUM_FORMS': '', # the max number of forms
 ...     'book_set-0-id': '1',
 ...     'book_set-0-title': 'Les Fleurs du Mal',
 ...     'book_set-1-id': '2',
@@ -697,7 +717,7 @@
 >>> 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_NUM_FORMS': '0', # the max number of forms
+...     'book_set-MAX_NUM_FORMS': '', # the max number of forms
 ...     'book_set-0-id': '5',
 ...     'book_set-0-title': 'Flowers of Evil',
 ...     'book_set-1-title': 'Revue des deux mondes',
@@ -734,7 +754,7 @@
 >>> data = {
 ...     'owner_set-TOTAL_FORMS': '2',
 ...     'owner_set-INITIAL_FORMS': '0',
-...     'owner_set-MAX_NUM_FORMS': '0',
+...     'owner_set-MAX_NUM_FORMS': '',
 ...     'owner_set-0-auto_id': '',
 ...     'owner_set-0-name': u'Joe Perry',
 ...     'owner_set-1-auto_id': '',
@@ -756,7 +776,7 @@
 >>> data = {
 ...     'owner_set-TOTAL_FORMS': '3',
 ...     'owner_set-INITIAL_FORMS': '1',
-...     'owner_set-MAX_NUM_FORMS': '0',
+...     'owner_set-MAX_NUM_FORMS': '',
 ...     'owner_set-0-auto_id': u'1',
 ...     'owner_set-0-name': u'Joe Perry',
 ...     'owner_set-1-auto_id': '',
@@ -848,7 +868,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '1',
 ...     'form-INITIAL_FORMS': '0',
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...     'form-0-slug': 'car-red',
 ... }
 >>> formset = FormSet(data)
@@ -860,7 +880,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '1',
 ...     'form-INITIAL_FORMS': '0',
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...     'form-0-slug': 'car-red',
 ... }
 >>> formset = FormSet(data)
@@ -875,7 +895,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '1',
 ...     'form-INITIAL_FORMS': '0',
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...     'form-0-price': u'12.00',
 ...     'form-0-quantity': '1',
 ... }
@@ -888,7 +908,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '1',
 ...     'form-INITIAL_FORMS': '0',
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...     'form-0-price': u'12.00',
 ...     'form-0-quantity': '1',
 ... }
@@ -906,7 +926,7 @@
 >>> data = {
 ...     'revision_set-TOTAL_FORMS': '1',
 ...     'revision_set-INITIAL_FORMS': '0',
-...     'revision_set-MAX_NUM_FORMS': '0',
+...     'revision_set-MAX_NUM_FORMS': '',
 ...     'revision_set-0-repository': repository.pk,
 ...     'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76',
 ...     'revision_set-0-DELETE': '',
@@ -921,7 +941,7 @@
 >>> data = {
 ...     'revision_set-TOTAL_FORMS': '1',
 ...     'revision_set-INITIAL_FORMS': '0',
-...     'revision_set-MAX_NUM_FORMS': '0',
+...     'revision_set-MAX_NUM_FORMS': '',
 ...     'revision_set-0-repository': repository.pk,
 ...     'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76',
 ...     'revision_set-0-DELETE': '',
@@ -939,7 +959,7 @@
 >>> data = {
 ...     'revision_set-TOTAL_FORMS': '1',
 ...     'revision_set-INITIAL_FORMS': '0',
-...     'revision_set-MAX_NUM_FORMS': '0',
+...     'revision_set-MAX_NUM_FORMS': '',
 ...     'revision_set-0-repository': repository.pk,
 ...     'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76',
 ...     'revision_set-0-DELETE': '',
@@ -969,7 +989,7 @@
 >>> data = {
 ...     'membership_set-TOTAL_FORMS': '1',
 ...     'membership_set-INITIAL_FORMS': '0',
-...     'membership_set-MAX_NUM_FORMS': '0',
+...     'membership_set-MAX_NUM_FORMS': '',
 ...     'membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d 
%H:%M:%S')),
 ...     'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d 
%H:%M:%S')),
 ...     'membership_set-0-karma': '',
@@ -984,7 +1004,7 @@
 >>> filled_data = {
 ...     'membership_set-TOTAL_FORMS': '1',
 ...     'membership_set-INITIAL_FORMS': '0',
-...     'membership_set-MAX_NUM_FORMS': '0',
+...     'membership_set-MAX_NUM_FORMS': '',
 ...     'membership_set-0-date_joined': 
unicode(one_day_later.strftime('%Y-%m-%d %H:%M:%S')),
 ...     'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d 
%H:%M:%S')),
 ...     'membership_set-0-karma': '',
@@ -1007,7 +1027,7 @@
 >>> data = {
 ...     'membership_set-TOTAL_FORMS': '1',
 ...     'membership_set-INITIAL_FORMS': '0',
-...     'membership_set-MAX_NUM_FORMS': '0',
+...     'membership_set-MAX_NUM_FORMS': '',
 ...     'membership_set-0-date_joined_0': unicode(now.strftime('%Y-%m-%d')),
 ...     'membership_set-0-date_joined_1': unicode(now.strftime('%H:%M:%S')),
 ...     'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d 
%H:%M:%S')),
@@ -1043,7 +1063,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': 2,
 ...     'form-INITIAL_FORMS': 0,
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...     'form-0-slug': 'red_car',
 ...     'form-1-slug': 'red_car',
 ... }
@@ -1057,7 +1077,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': 2,
 ...     'form-INITIAL_FORMS': 0,
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...     'form-0-price': '25',
 ...     'form-0-quantity': '7',
 ...     'form-1-price': '25',
@@ -1075,7 +1095,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '2',
 ...     'form-INITIAL_FORMS': '0',
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...     'form-0-price': '24',
 ...     'form-1-price': '24',
 ... }
@@ -1089,7 +1109,7 @@
 >>> data = {
 ...     'book_set-TOTAL_FORMS': '2',
 ...     'book_set-INITIAL_FORMS': '2',
-...     'book_set-MAX_NUM_FORMS': '0',
+...     'book_set-MAX_NUM_FORMS': '',
 ...
 ...     'book_set-0-title': 'The 2008 Election',
 ...     'book_set-0-author': str(author.id),
@@ -1111,7 +1131,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '2',
 ...     'form-INITIAL_FORMS': '0',
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...
 ...     'form-0-title': 'blah',
 ...     'form-0-slug': 'Morning',
@@ -1133,7 +1153,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '2',
 ...     'form-INITIAL_FORMS': '0',
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...
 ...     'form-0-title': 'foo',
 ...     'form-0-slug': 'Morning in Prague',
@@ -1153,7 +1173,7 @@
 >>> data = {
 ...     'form-TOTAL_FORMS': '2',
 ...     'form-INITIAL_FORMS': '0',
-...     'form-MAX_NUM_FORMS': '0',
+...     'form-MAX_NUM_FORMS': '',
 ...
 ...     'form-0-title': 'foo',
 ...     'form-0-slug': 'Morning in Prague',

Modified: django/trunk/tests/regressiontests/forms/formsets.py
===================================================================
--- django/trunk/tests/regressiontests/forms/formsets.py        2010-03-27 
23:03:12 UTC (rev 12871)
+++ django/trunk/tests/regressiontests/forms/formsets.py        2010-03-27 
23:03:56 UTC (rev 12872)
@@ -20,7 +20,7 @@
 
 >>> 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-MAX_NUM_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_NUM_FORMS" />
 <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>
 

Modified: django/trunk/tests/regressiontests/modeladmin/models.py
===================================================================
--- django/trunk/tests/regressiontests/modeladmin/models.py     2010-03-27 
23:03:12 UTC (rev 12871)
+++ django/trunk/tests/regressiontests/modeladmin/models.py     2010-03-27 
23:03:56 UTC (rev 12872)
@@ -919,7 +919,7 @@
 >>> validate(ValidationTestModelAdmin, ValidationTestModel)
 Traceback (most recent call last):
 ...
-ImproperlyConfigured: 'ValidationTestInline.max_num' should be a integer.
+ImproperlyConfigured: 'ValidationTestInline.max_num' should be an integer or 
None (default).
 
 >>> class ValidationTestInline(TabularInline):
 ...     model = ValidationTestInlineModel

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en.

Reply via email to