Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Made changes for #2445 requested by @timgraham on Feb 6, 2014.

- Moved ModelForm test of new ``limit_choices_to`` functionality to
  ``./tests/model_forms/*`` instead of keeping them in
  ``./tests/admin_views/*``.
- Added new test in admin_views for ``limit_choices_to`` callable
  functionality for Django Admin.
- Updated the release notes for Django 1.7 to include a note about the
  new behavior for ``limit_choices_to``.
- Added 'versionchanged' notification for ``limit_choices_to`` in the
  documentation for Django 1.7.
- Added explicit link to ``raw_id_fields`` in the changes to the
  ``ForeignKey.limit_choices_to`` documentation.
- Defined ModelForm for tests using more canonical 'fields' instead of
  'exclude'.
- Refactored datetime import statement for model_forms test.
- Phrased changes to documentation in a better way where requested.
- Added missing indentation for 'note' section of proposed changes to
  documentation.
- Simplified inline code comments where requested.
- Removed unused imports.
- Refs #2445.
  • Loading branch information...
commit 5d4b7a1c466174bfe05f32545652519446354c25 1 parent f822e2e
@adamsc64 adamsc64 authored
View
3  django/forms/models.py
@@ -324,8 +324,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
self._validate_unique = False
super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
error_class, label_suffix, empty_permitted)
- # This form instance should now have its own set of fields in
- # ``self.fields``. Apply the attribute 'limit_choices_to', if applicable.
+ # Apply ``limit_choices_to`` to each field.
for field_name in self.fields:
formfield = self.fields[field_name]
if hasattr(formfield, 'queryset'):
View
29 docs/ref/models/fields.txt
@@ -1087,25 +1087,32 @@ define the details of how the relation works.
causes the corresponding field on the ``ModelForm`` to list only ``Users``
that have ``is_staff=True``. This may be helpful in the Django admin.
- The callable form can be helpful when used in conjunction with the Python
- ``datetime`` module to limit selections by date range. For example::
+ The callable form can be helpful, for instance, when used in conjunction
+ with the Python ``datetime`` module to limit selections by date range. For
+ example::
limit_choices_to = lambda: {'pub_date__lte': datetime.date.utcnow()}
If ``limit_choices_to`` is or returns a :class:`Q object
<django.db.models.Q>`, which is useful for :ref:`complex queries
<complex-lookups-with-q>`, then it will only have an effect on the choices
- available in the admin when the field is not listed in ``raw_id_fields`` in
- the ``ModelAdmin`` for the model.
+ available in the admin when the field is not listed in
+ :attr:`~django.contrib.admin.ModelAdmin.raw_id_fields` in the
+ ``ModelAdmin`` for the model.
-.. note::
+ .. versionchanged:: 1.7
+
+ Previous versions of Django do not allow passing a callable as a value
+ for ``limit_choices_to``.
+
+ .. note::
- If a callable is used for ``limit_choices_to``, it will be invoked every time a
- new form is instantiated. It may also be invoked when a model is validated using
- various management commands, as well as in the admin. The admin constructs
- querysets to validate its form inputs in various edge cases multiple times,
- so there is a possibility your callable may be invoked that many times.
- Make sure there are no undesired side effects to this behavior in your case.
+ If a callable is used for ``limit_choices_to``, it will be invoked
+ every time a new form is instantiated. It may also be invoked when a
+ model is validated, for example by management commands or the admin.
+ The admin constructs querysets to validate its form inputs in various
+ edge cases multiple times, so there is a possibility your callable may
+ be invoked several times.
.. attribute:: ForeignKey.related_name
View
4 docs/releases/1.7.txt
@@ -538,6 +538,10 @@ Models
* It is now possible to use ``None`` as a query value for the :lookup:`iexact`
lookup.
+* It is now possible to pass a callable as value for the attribute
+ :attr:`ForeignKey.limit_choices_to` when defining a ``ForeignKey`` or
+ ``ManyToManyField``.
+
Signals
^^^^^^^
View
3  tests/admin_views/admin.py
@@ -34,7 +34,7 @@
UnchangeableObject, UserMessenger, Simple, Choice, ShortMessage, Telegram,
FilteredManager, EmptyModelHidden, EmptyModelVisible, EmptyModelMixin,
State, City, Restaurant, Worker, ParentWithDependentChildren,
- DependentChild)
+ DependentChild, StumpJoke)
def callable_year(dt_value):
@@ -884,6 +884,7 @@ class RestaurantAdmin(admin.ModelAdmin):
site.register(EmptyModelHidden, EmptyModelHiddenAdmin)
site.register(EmptyModelVisible, EmptyModelVisibleAdmin)
site.register(EmptyModelMixin, EmptyModelMixinAdmin)
+site.register(StumpJoke)
# Register core models we need in our tests
from django.contrib.auth.models import User, Group
View
8 tests/admin_views/forms.py
@@ -1,6 +1,5 @@
from django import forms
from django.contrib.admin.forms import AdminAuthenticationForm
-from .models import StumpJoke
class CustomAdminAuthenticationForm(AdminAuthenticationForm):
@@ -10,10 +9,3 @@ def clean_username(self):
if username == 'customform':
raise forms.ValidationError('custom form error')
return username
-
-
-class StumpJokeForm(forms.ModelForm):
-
- class Meta:
- model = StumpJoke
- exclude = []
View
41 tests/admin_views/models.py
@@ -11,7 +11,6 @@
)
from django.contrib.contenttypes.models import ContentType
from django.core.files.storage import FileSystemStorage
-from django.db.models import Q
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
@@ -174,6 +173,33 @@ def __str__(self):
return self.title
+def today_callable_dict():
+ return {"last_action__gte": datetime.datetime.today()}
+
+
+def today_callable_q():
+ return models.Q(last_action__gte=datetime.datetime.today())
+
+
+@python_2_unicode_compatible
+class Character(models.Model):
+ username = models.CharField(max_length=100)
+ last_action = models.DateTimeField()
+
+ def __str__(self):
+ return self.username
+
+
+@python_2_unicode_compatible
+class StumpJoke(models.Model):
+ variation = models.CharField(max_length=100)
+ most_recently_fooled = models.ForeignKey(Character, limit_choices_to=today_callable_dict, related_name="+")
+ has_fooled_today = models.ManyToManyField(Character, limit_choices_to=today_callable_q, related_name="+")
+
+ def __str__(self):
+ return self.variation
+
+
class Fabric(models.Model):
NG_CHOICES = (
('Textured', (
@@ -533,19 +559,6 @@ def __str__(self):
return self.answer
-def today_callable_dict():
- return {"date_joined__gte": datetime.datetime.today()}
-
-
-def today_callable_q():
- return Q(date_joined__gte=datetime.datetime.today())
-
-
-class StumpJoke(models.Model):
- most_recently_fooled = models.ForeignKey(User, limit_choices_to=today_callable_dict, related_name="+")
- has_fooled_today = models.ManyToManyField(User, limit_choices_to=today_callable_q, related_name="+")
-
-
class Reservation(models.Model):
start_date = models.DateTimeField()
price = models.IntegerField()
View
62 tests/admin_views/tests.py
@@ -53,9 +53,8 @@
Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject,
Simple, UndeletableObject, UnchangeableObject, Choice, ShortMessage,
Telegram, Pizza, Topping, FilteredManager, City, Restaurant, Worker,
- ParentWithDependentChildren, StumpJoke)
+ ParentWithDependentChildren, Character)
from .admin import site, site2, CityAdmin
-from .forms import StumpJokeForm
ERROR_MESSAGE = "Please enter the correct username and password \
@@ -3710,6 +3709,34 @@ def test_readonly_backwards_ref(self):
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class LimitChoicesToInAdminTest(TestCase):
+ urls = "admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_limit_choices_to_as_callable(self):
+ """Test for ticket 2445 changes to admin."""
+ character1 = Character(username='threepwood')
+ character2 = Character(username='marley')
+
+ character1.last_action = datetime.datetime.today() + datetime.timedelta(days=1)
+ character1.save()
+
+ character2.last_action = datetime.datetime.today() - datetime.timedelta(days=1)
+ character2.save()
+
+ response = self.client.get('/test_admin/admin/admin_views/stumpjoke/add/')
+ # The allowed option should appear twice; the limited option should not appear.
+ self.assertEqual(response.content.count('threepwood'), 2)
+ self.assertNotIn('marley', response.content)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class RawIdFieldsTest(TestCase):
urls = "admin_views.urls"
fixtures = ['admin-views-users.xml']
@@ -4780,34 +4807,3 @@ class GenericFKAdmin(ModelAdmin):
validator.validate_list_filter(GenericFKAdmin, Plot)
except ImproperlyConfigured:
self.fail("Couldn't validate a GenericRelation -> FK path in ModelAdmin.list_filter")
-
-
-class LimitChoicesToTest(TestCase):
- """
- Tests the functionality of ``limit_choices_to``.
- """
- def setUp(self):
- self.user1 = User.objects.create_user('threepwood')
- self.user2 = User.objects.create_user('marley')
-
- self.user1.date_joined = datetime.datetime.today() + datetime.timedelta(days=1)
- self.user1.save()
-
- self.user2.date_joined = datetime.datetime.today() - datetime.timedelta(days=1)
- self.user2.save()
-
- def test_limit_choices_to_callable_for_fk_rel(self):
- """
- A ForeignKey relation can use ``limit_choices_to`` as a callable.
- """
- stumpjokeform = StumpJokeForm()
- self.assertIn(self.user1, stumpjokeform.fields['most_recently_fooled'].queryset)
- self.assertNotIn(self.user2, stumpjokeform.fields['most_recently_fooled'].queryset)
-
- def test_limit_choices_to_callable_for_m2m_rel(self):
- """
- A ManyToMany relation can use ``limit_choices_to`` as a callable.
- """
- stumpjokeform = StumpJokeForm()
- self.assertIn(self.user1, stumpjokeform.fields['has_fooled_today'].queryset)
- self.assertNotIn(self.user2, stumpjokeform.fields['has_fooled_today'].queryset)
View
20 tests/model_forms/models.py
@@ -10,6 +10,7 @@
import os
import tempfile
+import datetime
from django.core import validators
from django.core.exceptions import ImproperlyConfigured, ValidationError
@@ -71,7 +72,6 @@ class Article(models.Model):
status = models.PositiveIntegerField(choices=ARTICLE_STATUS, blank=True, null=True)
def save(self):
- import datetime
if not self.id:
self.created = datetime.date.today()
return super(Article, self).save()
@@ -329,3 +329,21 @@ class CustomErrorMessage(models.Model):
def clean(self):
if self.name1 == 'FORBIDDEN_VALUE':
raise ValidationError({'name1': [ValidationError('Model.clean() error messages.')]})
+
+
+def today_callable_dict():
+ return {"last_action__gte": datetime.datetime.today()}
+
+
+def today_callable_q():
+ return models.Q(last_action__gte=datetime.datetime.today())
+
+
+class Character(models.Model):
+ username = models.CharField(max_length=100)
+ last_action = models.DateTimeField()
+
+
+class StumpJoke(models.Model):
+ most_recently_fooled = models.ForeignKey(Character, limit_choices_to=today_callable_dict, related_name="+")
+ has_fooled_today = models.ManyToManyField(Character, limit_choices_to=today_callable_q, related_name="+")
View
40 tests/model_forms/tests.py
@@ -22,7 +22,8 @@
DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle,
ImprovedArticleWithParentLink, Inventory, Post, Price,
Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem,
- ArticleStatusNote, DateTimePost, CustomErrorMessage, test_images)
+ ArticleStatusNote, DateTimePost, CustomErrorMessage, test_images,
+ StumpJoke, Character)
if test_images:
from .models import ImageFile, OptionalImageFile
@@ -521,6 +522,12 @@ class Meta:
}
+class StumpJokeForm(forms.ModelForm):
+ class Meta:
+ model = StumpJoke
+ fields = '__all__'
+
+
class TestFieldOverridesTroughFormMeta(TestCase):
def test_widget_overrides(self):
form = FieldOverridesTroughFormMetaForm()
@@ -1878,3 +1885,34 @@ class Form2(forms.Form):
self.assertEqual(list(type(str('NewForm'), (ModelForm, Mixin, Form), {})().fields.keys()), ['name'])
self.assertEqual(list(type(str('NewForm'), (ModelForm, Form, Mixin), {})().fields.keys()), ['name', 'age'])
self.assertEqual(list(type(str('NewForm'), (ModelForm, Form), {'age': None})().fields.keys()), ['name'])
+
+
+class LimitChoicesToTest(TestCase):
+ """
+ Tests the functionality of ``limit_choices_to``.
+ """
+ def setUp(self):
+ self.user1 = Character(username='threepwood')
+ self.user2 = Character(username='marley')
+
+ self.user1.last_action = datetime.datetime.today() + datetime.timedelta(days=1)
+ self.user1.save()
+
+ self.user2.last_action = datetime.datetime.today() - datetime.timedelta(days=1)
+ self.user2.save()
+
+ def test_limit_choices_to_callable_for_fk_rel(self):
+ """
+ A ForeignKey relation can use ``limit_choices_to`` as a callable, re #2554.
+ """
+ stumpjokeform = StumpJokeForm()
+ self.assertIn(self.user1, stumpjokeform.fields['most_recently_fooled'].queryset)
+ self.assertNotIn(self.user2, stumpjokeform.fields['most_recently_fooled'].queryset)
+
+ def test_limit_choices_to_callable_for_m2m_rel(self):
+ """
+ A ManyToMany relation can use ``limit_choices_to`` as a callable, re #2554.
+ """
+ stumpjokeform = StumpJokeForm()
+ self.assertIn(self.user1, stumpjokeform.fields['has_fooled_today'].queryset)
+ self.assertNotIn(self.user2, stumpjokeform.fields['has_fooled_today'].queryset)
Please sign in to comment.
Something went wrong with that request. Please try again.