From aba95dcc0b5370ffac3d3b701c3ca7782ee999c1 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Sat, 27 Mar 2010 23:03:56 +0000 Subject: [PATCH] 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. git-svn-id: http://code.djangoproject.com/svn/django/trunk@12872 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- AUTHORS | 1 + django/contrib/admin/media/js/inlines.js | 9 +- django/contrib/admin/media/js/inlines.min.js | 6 +- django/contrib/admin/options.py | 2 +- django/contrib/admin/validation.py | 14 ++- django/contrib/contenttypes/generic.py | 2 +- django/forms/formsets.py | 15 ++- django/forms/models.py | 12 +-- docs/topics/forms/formsets.txt | 28 +++-- docs/topics/forms/modelforms.txt | 31 +++--- tests/modeltests/model_formsets/models.py | 102 +++++++++++-------- tests/regressiontests/forms/formsets.py | 2 +- tests/regressiontests/modeladmin/models.py | 2 +- 13 files changed, 138 insertions(+), 88 deletions(-) diff --git a/AUTHORS b/AUTHORS index 62dbeb1161..b06bbd6977 100644 --- a/AUTHORS +++ b/AUTHORS @@ -225,6 +225,7 @@ answer newbie questions, and generally made Django that much better: John Huddleston Rob Hudson Jason Huggins + Gabriel Hurley Hyun Mi Ae Ibon Tom Insam diff --git a/django/contrib/admin/media/js/inlines.js b/django/contrib/admin/media/js/inlines.js index 7f12c0db3c..55118dfc4b 100644 --- a/django/contrib/admin/media/js/inlines.js +++ b/django/contrib/admin/media/js/inlines.js @@ -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 diff --git a/django/contrib/admin/media/js/inlines.min.js b/django/contrib/admin/media/js/inlines.min.js index 6fc4f82b8d..29e048df54 100644 --- a/django/contrib/admin/media/js/inlines.min.js +++ b/django/contrib/admin/media/js/inlines.min.js @@ -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(''+b.addText+"");i=a(this).parent().find("tr:last a")}else{a(this).filter(":last").after('");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('");else a(c).is("UL")||a(c).is("OL")?a(c).append('
  • '+b.deleteText+"
  • "): -a(c).children(":first").append(''+b.deleteText+"");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('");else a(c).is("UL")||a(c).is("OL")?a(c).append('
  • '+b.deleteText+"
  • "): +a(c).children(":first").append(''+b.deleteText+"");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 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 @@ class BaseFormSet(StrAndUnicode): 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 @@ 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, 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, diff --git a/django/forms/models.py b/django/forms/models.py index 869138c922..608b87bd33 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -448,10 +448,10 @@ class BaseModelFormSet(BaseFormSet): 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 @@ class BaseModelFormSet(BaseFormSet): 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 _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. diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 3fb2b0af16..d65daa5dd5 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -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() @@ -80,8 +80,20 @@ force the maximum number of forms the formset will display:: -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 @@ 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', @@ -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', @@ -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', @@ -287,7 +299,7 @@ Lets create a formset with the ability to delete:: ... ]) >>> for form in formset.forms: .... print form.as_table() - + @@ -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', diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 9a64919534..1e9c9db541 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -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 @@ -401,7 +401,7 @@ field, you could do the following:: class ArticleForm(ModelForm): pub_date = MyDateFormField() - + class Meta: model = Article @@ -557,7 +557,7 @@ with the ``Author`` model. It works just like a regular formset:: >>> formset = AuthorFormSet() >>> print formset - + +.. 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 ------------------------------- diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index ba8592d909..b67df97d76 100644 --- a/tests/modeltests/model_formsets/models.py +++ b/tests/modeltests/model_formsets/models.py @@ -200,7 +200,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_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 @@ 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_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 @@ 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_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 @@ 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_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 @@ Test the behavior of commit=False and save_m2m >>> 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,18 +365,38 @@ True # 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'] @@ -398,7 +418,7 @@ used. >>> 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 @@ True >>> 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 @@ True >>> 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 @@ 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_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 @@ 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_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 @@ 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_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 @@ Test inline formsets where the inline-edited object has a custom primary key tha >>> 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 @@ has a non AutoField yet auto-created primary key. >>> 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 @@ True >>> 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 @@ We can provide a custom queryset to our InlineFormSet: >>> 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 @@ True >>> 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 @@ We need to ensure that it is displayed >>> 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 @@ True >>> 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 @@ True >>> 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 @@ True >>> 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 @@ False >>> 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 @@ True >>> 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 @@ False >>> 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 @@ True >>> 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 @@ False >>> 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 @@ False >>> 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 @@ True >>> 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 @@ False >>> 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 @@ True >>> 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 @@ False >>> 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 @@ False >>> 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 @@ True >>> 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 @@ False >>> 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 @@ False >>> 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 @@ False >>> 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', diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py index 3eecb7f7be..ec7fc9ba11 100644 --- a/tests/regressiontests/forms/formsets.py +++ b/tests/regressiontests/forms/formsets.py @@ -20,7 +20,7 @@ but we'll look at how to do so later. >>> formset = ChoiceFormSet(auto_id=False, prefix='choices') >>> print formset - + Choice: Votes: diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py index ae5c1aecf0..36ea416e7c 100644 --- a/tests/regressiontests/modeladmin/models.py +++ b/tests/regressiontests/modeladmin/models.py @@ -919,7 +919,7 @@ ImproperlyConfigured: 'ValidationTestInline.extra' should be a integer. >>> 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