diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index f2723a18c76..679b37cfca1 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -948,7 +948,10 @@ class ManyToManyField(RelatedField, Field): # If initial is passed in, it's a list of related objects, but the # MultipleChoiceField takes a list of IDs. 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) def db_type(self): diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index 16b50145d76..c5795b7e561 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -613,6 +613,30 @@ Add some categories and test the many-to-many form output. Hold down "Control", or "Command" on a Mac, to select more than one. +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() +
  • Headline:
  • +
  • Slug:
  • +
  • Pub date:
  • +
  • Writer:
  • +
  • Article:
  • +
  • Status:
  • +
  • Categories: Hold down "Control", or "Command" on a Mac, to select more than one.
  • + >>> 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) >>> new_art = f.save() diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index 642cd203ca6..bf9623fe771 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -1146,37 +1146,63 @@ possible to specify callable data. >>> class UserRegistration(Form): ... username = CharField(max_length=10) ... password = CharField(widget=PasswordInput) +... options = MultipleChoiceField(choices=[('f','foo'),('b','bar'),('w','whiz')]) We need to define functions that get called later. >>> def initial_django(): ... return 'django' >>> def initial_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. ->>> 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()
  • Username:
  • Password:
  • +
  • Options:
  • 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()
  • Username:
  • Password:
  • +
  • Options:
  • >>> p = UserRegistration({'username': u''}, initial={'username': initial_django}, auto_id=False) >>> print p.as_ul()
  • Username:
  • Password:
  • ->>> p = UserRegistration({'username': u'foo'}, initial={'username': initial_django}, auto_id=False) +
  • Options:
  • +>>> p = UserRegistration({'username': u'foo', 'options':['f','b']}, initial={'username': initial_django}, auto_id=False) >>> print p.as_ul()
  • Username:
  • Password:
  • +
  • Options:
  • 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 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'] [u'This field is required.'] >>> p.is_valid() @@ -1187,14 +1213,26 @@ then the latter will get precedence. >>> class UserRegistration(Form): ... username = CharField(max_length=10, initial=initial_django) ... password = CharField(widget=PasswordInput) +... options = MultipleChoiceField(choices=[('f','foo'),('b','bar'),('w','whiz')], initial=initial_other_options) + >>> p = UserRegistration(auto_id=False) >>> print p.as_ul()
  • Username:
  • Password:
  • ->>> p = UserRegistration(initial={'username': initial_stephane}, auto_id=False) +
  • Options:
  • +>>> p = UserRegistration(initial={'username': initial_stephane, 'options': initial_options}, auto_id=False) >>> print p.as_ul()
  • Username:
  • Password:
  • +
  • Options:
  • # Help text ################################################################### diff --git a/tests/regressiontests/model_forms_regress/models.py b/tests/regressiontests/model_forms_regress/models.py index 50a2d615316..8c3f97e9475 100644 --- a/tests/regressiontests/model_forms_regress/models.py +++ b/tests/regressiontests/model_forms_regress/models.py @@ -14,3 +14,17 @@ class Triple(models.Model): class FilePathModel(models.Model): 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 diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests.py index 6547367812b..56d04af0610 100644 --- a/tests/regressiontests/model_forms_regress/tests.py +++ b/tests/regressiontests/model_forms_regress/tests.py @@ -1,18 +1,22 @@ +from datetime import date + from django import db from django import forms +from django.forms.models import modelform_factory from django.conf import settings from django.test import TestCase -from models import Person, Triple, FilePathModel + +from models import Person, Triple, FilePathModel, Article, Publication class ModelMultipleChoiceFieldTests(TestCase): - + def setUp(self): self.old_debug = settings.DEBUG settings.DEBUG = True - + def tearDown(self): settings.DEBUG = self.old_debug - + def test_model_multiple_choice_number_of_queries(self): """ Test that ModelMultipleChoiceField does O(1) queries instead of @@ -20,11 +24,11 @@ class ModelMultipleChoiceFieldTests(TestCase): """ for i in range(30): Person.objects.create(name="Person %s" % i) - + db.reset_queries() f = forms.ModelMultipleChoiceField(queryset=Person.objects.all()) 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 Meta: @@ -59,3 +63,30 @@ class FilePathFieldTests(TestCase): names = [p[1] for p in form['path'].field.choices] names.sort() 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"""
  • +
  • Hold down "Control", or "Command" on a Mac, to select more than one.
  • """) + +