Fixed #10349 -- Modified ManyToManyFields to allow initial form values to be callables. Thanks to fas for the report and patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@10652 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2009-05-02 07:03:33 +00:00
parent e43ace720c
commit fbf5eaac94
5 changed files with 122 additions and 12 deletions

View File

@ -948,7 +948,10 @@ class ManyToManyField(RelatedField, Field):
# If initial is passed in, it's a list of related objects, but the # If initial is passed in, it's a list of related objects, but the
# MultipleChoiceField takes a list of IDs. # MultipleChoiceField takes a list of IDs.
if defaults.get('initial') is not None: if defaults.get('initial') is not None:
defaults['initial'] = [i._get_pk_val() for i in defaults['initial']] initial = defaults['initial']
if callable(initial):
initial = initial()
defaults['initial'] = [i._get_pk_val() for i in initial]
return super(ManyToManyField, self).formfield(**defaults) return super(ManyToManyField, self).formfield(**defaults)
def db_type(self): def db_type(self):

View File

@ -613,6 +613,30 @@ Add some categories and test the many-to-many form output.
<option value="3">Third test</option> <option value="3">Third test</option>
</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li> </select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
Initial values can be provided for model forms
>>> f = TestArticleForm(auto_id=False, initial={'headline': 'Your headline here', 'categories': ['1','2']})
>>> print f.as_ul()
<li>Headline: <input type="text" name="headline" value="Your headline here" maxlength="50" /></li>
<li>Slug: <input type="text" name="slug" maxlength="50" /></li>
<li>Pub date: <input type="text" name="pub_date" /></li>
<li>Writer: <select name="writer">
<option value="" selected="selected">---------</option>
<option value="1">Mike Royko</option>
<option value="2">Bob Woodward</option>
</select></li>
<li>Article: <textarea rows="10" cols="40" name="article"></textarea></li>
<li>Status: <select name="status">
<option value="" selected="selected">---------</option>
<option value="1">Draft</option>
<option value="2">Pending</option>
<option value="3">Live</option>
</select></li>
<li>Categories: <select multiple="multiple" name="categories">
<option value="1" selected="selected">Entertainment</option>
<option value="2" selected="selected">It&#39;s a test</option>
<option value="3">Third test</option>
</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
>>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04', >>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04',
... 'writer': u'1', 'article': u'Hello.', 'categories': [u'1', u'2']}, instance=new_art) ... 'writer': u'1', 'article': u'Hello.', 'categories': [u'1', u'2']}, instance=new_art)
>>> new_art = f.save() >>> new_art = f.save()

View File

@ -1146,37 +1146,63 @@ possible to specify callable data.
>>> class UserRegistration(Form): >>> class UserRegistration(Form):
... username = CharField(max_length=10) ... username = CharField(max_length=10)
... password = CharField(widget=PasswordInput) ... password = CharField(widget=PasswordInput)
... options = MultipleChoiceField(choices=[('f','foo'),('b','bar'),('w','whiz')])
We need to define functions that get called later. We need to define functions that get called later.
>>> def initial_django(): >>> def initial_django():
... return 'django' ... return 'django'
>>> def initial_stephane(): >>> def initial_stephane():
... return 'stephane' ... return 'stephane'
>>> def initial_options():
... return ['f','b']
>>> def initial_other_options():
... return ['b','w']
Here, we're not submitting any data, so the initial value will be displayed. Here, we're not submitting any data, so the initial value will be displayed.
>>> p = UserRegistration(initial={'username': initial_django}, auto_id=False) >>> p = UserRegistration(initial={'username': initial_django, 'options': initial_options}, auto_id=False)
>>> print p.as_ul() >>> print p.as_ul()
<li>Username: <input type="text" name="username" value="django" maxlength="10" /></li> <li>Username: <input type="text" name="username" value="django" maxlength="10" /></li>
<li>Password: <input type="password" name="password" /></li> <li>Password: <input type="password" name="password" /></li>
<li>Options: <select multiple="multiple" name="options">
<option value="f" selected="selected">foo</option>
<option value="b" selected="selected">bar</option>
<option value="w">whiz</option>
</select></li>
The 'initial' parameter is meaningless if you pass data. The 'initial' parameter is meaningless if you pass data.
>>> p = UserRegistration({}, initial={'username': initial_django}, auto_id=False) >>> p = UserRegistration({}, initial={'username': initial_django, 'options': initial_options}, auto_id=False)
>>> print p.as_ul() >>> print p.as_ul()
<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li> <li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li>
<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li> <li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>
<li><ul class="errorlist"><li>This field is required.</li></ul>Options: <select multiple="multiple" name="options">
<option value="f">foo</option>
<option value="b">bar</option>
<option value="w">whiz</option>
</select></li>
>>> p = UserRegistration({'username': u''}, initial={'username': initial_django}, auto_id=False) >>> p = UserRegistration({'username': u''}, initial={'username': initial_django}, auto_id=False)
>>> print p.as_ul() >>> print p.as_ul()
<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li> <li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li>
<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li> <li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>
>>> p = UserRegistration({'username': u'foo'}, initial={'username': initial_django}, auto_id=False) <li><ul class="errorlist"><li>This field is required.</li></ul>Options: <select multiple="multiple" name="options">
<option value="f">foo</option>
<option value="b">bar</option>
<option value="w">whiz</option>
</select></li>
>>> p = UserRegistration({'username': u'foo', 'options':['f','b']}, initial={'username': initial_django}, auto_id=False)
>>> print p.as_ul() >>> print p.as_ul()
<li>Username: <input type="text" name="username" value="foo" maxlength="10" /></li> <li>Username: <input type="text" name="username" value="foo" maxlength="10" /></li>
<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li> <li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>
<li>Options: <select multiple="multiple" name="options">
<option value="f" selected="selected">foo</option>
<option value="b" selected="selected">bar</option>
<option value="w">whiz</option>
</select></li>
A callable 'initial' value is *not* used as a fallback if data is not provided. A callable 'initial' value is *not* used as a fallback if data is not provided.
In this example, we don't provide a value for 'username', and the form raises a In this example, we don't provide a value for 'username', and the form raises a
validation error rather than using the initial value for 'username'. validation error rather than using the initial value for 'username'.
>>> p = UserRegistration({'password': 'secret'}, initial={'username': initial_django}) >>> p = UserRegistration({'password': 'secret'}, initial={'username': initial_django, 'options': initial_options})
>>> p.errors['username'] >>> p.errors['username']
[u'This field is required.'] [u'This field is required.']
>>> p.is_valid() >>> p.is_valid()
@ -1187,14 +1213,26 @@ then the latter will get precedence.
>>> class UserRegistration(Form): >>> class UserRegistration(Form):
... username = CharField(max_length=10, initial=initial_django) ... username = CharField(max_length=10, initial=initial_django)
... password = CharField(widget=PasswordInput) ... password = CharField(widget=PasswordInput)
... options = MultipleChoiceField(choices=[('f','foo'),('b','bar'),('w','whiz')], initial=initial_other_options)
>>> p = UserRegistration(auto_id=False) >>> p = UserRegistration(auto_id=False)
>>> print p.as_ul() >>> print p.as_ul()
<li>Username: <input type="text" name="username" value="django" maxlength="10" /></li> <li>Username: <input type="text" name="username" value="django" maxlength="10" /></li>
<li>Password: <input type="password" name="password" /></li> <li>Password: <input type="password" name="password" /></li>
>>> p = UserRegistration(initial={'username': initial_stephane}, auto_id=False) <li>Options: <select multiple="multiple" name="options">
<option value="f">foo</option>
<option value="b" selected="selected">bar</option>
<option value="w" selected="selected">whiz</option>
</select></li>
>>> p = UserRegistration(initial={'username': initial_stephane, 'options': initial_options}, auto_id=False)
>>> print p.as_ul() >>> print p.as_ul()
<li>Username: <input type="text" name="username" value="stephane" maxlength="10" /></li> <li>Username: <input type="text" name="username" value="stephane" maxlength="10" /></li>
<li>Password: <input type="password" name="password" /></li> <li>Password: <input type="password" name="password" /></li>
<li>Options: <select multiple="multiple" name="options">
<option value="f" selected="selected">foo</option>
<option value="b" selected="selected">bar</option>
<option value="w">whiz</option>
</select></li>
# Help text ################################################################### # Help text ###################################################################

View File

@ -14,3 +14,17 @@ class Triple(models.Model):
class FilePathModel(models.Model): class FilePathModel(models.Model):
path = models.FilePathField(path=os.path.dirname(__file__), match=".*\.py$", blank=True) path = models.FilePathField(path=os.path.dirname(__file__), match=".*\.py$", blank=True)
class Publication(models.Model):
title = models.CharField(max_length=30)
date = models.DateField()
def __unicode__(self):
return self.title
class Article(models.Model):
headline = models.CharField(max_length=100)
publications = models.ManyToManyField(Publication)
def __unicode__(self):
return self.headline

View File

@ -1,18 +1,22 @@
from datetime import date
from django import db from django import db
from django import forms from django import forms
from django.forms.models import modelform_factory
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from models import Person, Triple, FilePathModel
from models import Person, Triple, FilePathModel, Article, Publication
class ModelMultipleChoiceFieldTests(TestCase): class ModelMultipleChoiceFieldTests(TestCase):
def setUp(self): def setUp(self):
self.old_debug = settings.DEBUG self.old_debug = settings.DEBUG
settings.DEBUG = True settings.DEBUG = True
def tearDown(self): def tearDown(self):
settings.DEBUG = self.old_debug settings.DEBUG = self.old_debug
def test_model_multiple_choice_number_of_queries(self): def test_model_multiple_choice_number_of_queries(self):
""" """
Test that ModelMultipleChoiceField does O(1) queries instead of Test that ModelMultipleChoiceField does O(1) queries instead of
@ -20,11 +24,11 @@ class ModelMultipleChoiceFieldTests(TestCase):
""" """
for i in range(30): for i in range(30):
Person.objects.create(name="Person %s" % i) Person.objects.create(name="Person %s" % i)
db.reset_queries() db.reset_queries()
f = forms.ModelMultipleChoiceField(queryset=Person.objects.all()) f = forms.ModelMultipleChoiceField(queryset=Person.objects.all())
selected = f.clean([1, 3, 5, 7, 9]) selected = f.clean([1, 3, 5, 7, 9])
self.assertEquals(len(db.connection.queries), 1) self.assertEquals(len(db.connection.queries), 1)
class TripleForm(forms.ModelForm): class TripleForm(forms.ModelForm):
class Meta: class Meta:
@ -59,3 +63,30 @@ class FilePathFieldTests(TestCase):
names = [p[1] for p in form['path'].field.choices] names = [p[1] for p in form['path'].field.choices]
names.sort() names.sort()
self.assertEqual(names, ['---------', '__init__.py', 'models.py', 'tests.py']) self.assertEqual(names, ['---------', '__init__.py', 'models.py', 'tests.py'])
class ManyToManyCallableInitialTests(TestCase):
def test_callable(self):
"Regression for #10349: A callable can be provided as the initial value for an m2m field"
# Set up a callable initial value
def formfield_for_dbfield(db_field, **kwargs):
if db_field.name == 'publications':
kwargs['initial'] = lambda: Publication.objects.all().order_by('date')[:2]
return db_field.formfield(**kwargs)
# Set up some Publications to use as data
Publication(title="First Book", date=date(2007,1,1)).save()
Publication(title="Second Book", date=date(2008,1,1)).save()
Publication(title="Third Book", date=date(2009,1,1)).save()
# Create a ModelForm, instantiate it, and check that the output is as expected
ModelForm = modelform_factory(Article, formfield_callback=formfield_for_dbfield)
form = ModelForm()
self.assertEquals(form.as_ul(), u"""<li><label for="id_headline">Headline:</label> <input id="id_headline" type="text" name="headline" maxlength="100" /></li>
<li><label for="id_publications">Publications:</label> <select multiple="multiple" name="publications" id="id_publications">
<option value="1" selected="selected">First Book</option>
<option value="2" selected="selected">Second Book</option>
<option value="3">Third Book</option>
</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>""")