Fixed #2445 -- Allowed limit_choices_to attribute to be a callable.
ForeignKey or ManyToManyField attribute ``limit_choices_to`` can now be a callable that returns either a ``Q`` object or a dict. Thanks michael at actrix.gen.nz for the original suggestion.
This commit is contained in:
parent
a718fcf201
commit
eefc88feef
2
AUTHORS
2
AUTHORS
|
@ -57,7 +57,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
|
|
||||||
Gisle Aas <gisle@aas.no>
|
Gisle Aas <gisle@aas.no>
|
||||||
Chris Adams
|
Chris Adams
|
||||||
Christopher Adams <christopher.r.adams@gmail.com>
|
Christopher Adams <http://christopheradams.info>
|
||||||
Mathieu Agopian <mathieu.agopian@gmail.com>
|
Mathieu Agopian <mathieu.agopian@gmail.com>
|
||||||
Roberto Aguilar <roberto@baremetal.io>
|
Roberto Aguilar <roberto@baremetal.io>
|
||||||
ajs <adi@sieker.info>
|
ajs <adi@sieker.info>
|
||||||
|
|
|
@ -240,7 +240,7 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
|
||||||
if related_admin is not None:
|
if related_admin is not None:
|
||||||
ordering = related_admin.get_ordering(request)
|
ordering = related_admin.get_ordering(request)
|
||||||
if ordering is not None and ordering != ():
|
if ordering is not None and ordering != ():
|
||||||
return db_field.rel.to._default_manager.using(db).order_by(*ordering).complex_filter(db_field.rel.limit_choices_to)
|
return db_field.rel.to._default_manager.using(db).order_by(*ordering)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
|
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
|
||||||
|
@ -383,6 +383,9 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
|
||||||
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
|
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
|
||||||
# are allowed to work.
|
# are allowed to work.
|
||||||
for l in model._meta.related_fkey_lookups:
|
for l in model._meta.related_fkey_lookups:
|
||||||
|
# As ``limit_choices_to`` can be a callable, invoke it here.
|
||||||
|
if callable(l):
|
||||||
|
l = l()
|
||||||
for k, v in widgets.url_params_from_lookup_dict(l).items():
|
for k, v in widgets.url_params_from_lookup_dict(l).items():
|
||||||
if k == lookup and v == value:
|
if k == lookup and v == value:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -459,17 +459,17 @@ def get_limit_choices_to_from_path(model, path):
|
||||||
""" Return Q object for limiting choices if applicable.
|
""" Return Q object for limiting choices if applicable.
|
||||||
|
|
||||||
If final model in path is linked via a ForeignKey or ManyToManyField which
|
If final model in path is linked via a ForeignKey or ManyToManyField which
|
||||||
has a `limit_choices_to` attribute, return it as a Q object.
|
has a ``limit_choices_to`` attribute, return it as a Q object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fields = get_fields_from_path(model, path)
|
fields = get_fields_from_path(model, path)
|
||||||
fields = remove_trailing_data_field(fields)
|
fields = remove_trailing_data_field(fields)
|
||||||
limit_choices_to = (
|
get_limit_choices_to = (
|
||||||
fields and hasattr(fields[-1], 'rel') and
|
fields and hasattr(fields[-1], 'rel') and
|
||||||
getattr(fields[-1].rel, 'limit_choices_to', None))
|
getattr(fields[-1].rel, 'get_limit_choices_to', None))
|
||||||
if not limit_choices_to:
|
if not get_limit_choices_to:
|
||||||
return models.Q() # empty Q
|
return models.Q() # empty Q
|
||||||
elif isinstance(limit_choices_to, models.Q):
|
limit_choices_to = get_limit_choices_to()
|
||||||
|
if isinstance(limit_choices_to, models.Q):
|
||||||
return limit_choices_to # already a Q
|
return limit_choices_to # already a Q
|
||||||
else:
|
else:
|
||||||
return models.Q(**limit_choices_to) # convert dict to Q
|
return models.Q(**limit_choices_to) # convert dict to Q
|
||||||
|
|
|
@ -180,7 +180,10 @@ class ForeignKeyRawIdWidget(forms.TextInput):
|
||||||
return mark_safe(''.join(output))
|
return mark_safe(''.join(output))
|
||||||
|
|
||||||
def base_url_parameters(self):
|
def base_url_parameters(self):
|
||||||
return url_params_from_lookup_dict(self.rel.limit_choices_to)
|
limit_choices_to = self.rel.limit_choices_to
|
||||||
|
if callable(limit_choices_to):
|
||||||
|
limit_choices_to = limit_choices_to()
|
||||||
|
return url_params_from_lookup_dict(limit_choices_to)
|
||||||
|
|
||||||
def url_parameters(self):
|
def url_parameters(self):
|
||||||
from django.contrib.admin.views.main import TO_FIELD_VAR
|
from django.contrib.admin.views.main import TO_FIELD_VAR
|
||||||
|
|
|
@ -742,11 +742,11 @@ class Field(RegisterLookupMixin):
|
||||||
lst = [(getattr(x, self.rel.get_related_field().attname),
|
lst = [(getattr(x, self.rel.get_related_field().attname),
|
||||||
smart_text(x))
|
smart_text(x))
|
||||||
for x in rel_model._default_manager.complex_filter(
|
for x in rel_model._default_manager.complex_filter(
|
||||||
self.rel.limit_choices_to)]
|
self.get_limit_choices_to())]
|
||||||
else:
|
else:
|
||||||
lst = [(x._get_pk_val(), smart_text(x))
|
lst = [(x._get_pk_val(), smart_text(x))
|
||||||
for x in rel_model._default_manager.complex_filter(
|
for x in rel_model._default_manager.complex_filter(
|
||||||
self.rel.limit_choices_to)]
|
self.get_limit_choices_to())]
|
||||||
return first_choice + lst
|
return first_choice + lst
|
||||||
|
|
||||||
def get_choices_default(self):
|
def get_choices_default(self):
|
||||||
|
|
|
@ -309,6 +309,35 @@ class RelatedField(Field):
|
||||||
if not cls._meta.abstract:
|
if not cls._meta.abstract:
|
||||||
self.contribute_to_related_class(other, self.related)
|
self.contribute_to_related_class(other, self.related)
|
||||||
|
|
||||||
|
def get_limit_choices_to(self):
|
||||||
|
"""Returns 'limit_choices_to' for this model field.
|
||||||
|
|
||||||
|
If it is a callable, it will be invoked and the result will be
|
||||||
|
returned.
|
||||||
|
"""
|
||||||
|
if callable(self.rel.limit_choices_to):
|
||||||
|
return self.rel.limit_choices_to()
|
||||||
|
return self.rel.limit_choices_to
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
"""Passes ``limit_choices_to`` to field being constructed.
|
||||||
|
|
||||||
|
Only passes it if there is a type that supports related fields.
|
||||||
|
This is a similar strategy used to pass the ``queryset`` to the field
|
||||||
|
being constructed.
|
||||||
|
"""
|
||||||
|
defaults = {}
|
||||||
|
if hasattr(self.rel, 'get_related_field'):
|
||||||
|
# If this is a callable, do not invoke it here. Just pass
|
||||||
|
# it in the defaults for when the form class will later be
|
||||||
|
# instantiated.
|
||||||
|
limit_choices_to = self.rel.limit_choices_to
|
||||||
|
defaults.update({
|
||||||
|
'limit_choices_to': limit_choices_to,
|
||||||
|
})
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return super(RelatedField, self).formfield(**defaults)
|
||||||
|
|
||||||
def related_query_name(self):
|
def related_query_name(self):
|
||||||
# This method defines the name that can be used to identify this
|
# This method defines the name that can be used to identify this
|
||||||
# related object in a table-spanning query. It uses the lower-cased
|
# related object in a table-spanning query. It uses the lower-cased
|
||||||
|
@ -1525,6 +1554,9 @@ class ForeignObject(RelatedField):
|
||||||
# and swapped models don't get a related descriptor.
|
# and swapped models don't get a related descriptor.
|
||||||
if not self.rel.is_hidden() and not related.model._meta.swapped:
|
if not self.rel.is_hidden() and not related.model._meta.swapped:
|
||||||
setattr(cls, related.get_accessor_name(), self.related_accessor_class(related))
|
setattr(cls, related.get_accessor_name(), self.related_accessor_class(related))
|
||||||
|
# While 'limit_choices_to' might be a callable, simply pass
|
||||||
|
# it along for later - this is too early because it's still
|
||||||
|
# model load time.
|
||||||
if self.rel.limit_choices_to:
|
if self.rel.limit_choices_to:
|
||||||
cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to)
|
cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to)
|
||||||
|
|
||||||
|
@ -1633,7 +1665,7 @@ class ForeignKey(ForeignObject):
|
||||||
qs = self.rel.to._default_manager.using(using).filter(
|
qs = self.rel.to._default_manager.using(using).filter(
|
||||||
**{self.rel.field_name: value}
|
**{self.rel.field_name: value}
|
||||||
)
|
)
|
||||||
qs = qs.complex_filter(self.rel.limit_choices_to)
|
qs = qs.complex_filter(self.get_limit_choices_to())
|
||||||
if not qs.exists():
|
if not qs.exists():
|
||||||
raise exceptions.ValidationError(
|
raise exceptions.ValidationError(
|
||||||
self.error_messages['invalid'],
|
self.error_messages['invalid'],
|
||||||
|
@ -1691,7 +1723,7 @@ class ForeignKey(ForeignObject):
|
||||||
(self.name, self.rel.to))
|
(self.name, self.rel.to))
|
||||||
defaults = {
|
defaults = {
|
||||||
'form_class': forms.ModelChoiceField,
|
'form_class': forms.ModelChoiceField,
|
||||||
'queryset': self.rel.to._default_manager.using(db).complex_filter(self.rel.limit_choices_to),
|
'queryset': self.rel.to._default_manager.using(db),
|
||||||
'to_field_name': self.rel.field_name,
|
'to_field_name': self.rel.field_name,
|
||||||
}
|
}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
|
@ -2127,7 +2159,7 @@ class ManyToManyField(RelatedField):
|
||||||
db = kwargs.pop('using', None)
|
db = kwargs.pop('using', None)
|
||||||
defaults = {
|
defaults = {
|
||||||
'form_class': forms.ModelMultipleChoiceField,
|
'form_class': forms.ModelMultipleChoiceField,
|
||||||
'queryset': self.rel.to._default_manager.using(db).complex_filter(self.rel.limit_choices_to)
|
'queryset': self.rel.to._default_manager.using(db),
|
||||||
}
|
}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
# 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
|
||||||
|
|
|
@ -170,6 +170,17 @@ class Field(object):
|
||||||
"""
|
"""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def get_limit_choices_to(self):
|
||||||
|
"""
|
||||||
|
Returns ``limit_choices_to`` for this form field.
|
||||||
|
|
||||||
|
If it is a callable, it will be invoked and the result will be
|
||||||
|
returned.
|
||||||
|
"""
|
||||||
|
if callable(self.limit_choices_to):
|
||||||
|
return self.limit_choices_to()
|
||||||
|
return self.limit_choices_to
|
||||||
|
|
||||||
def _has_changed(self, initial, data):
|
def _has_changed(self, initial, data):
|
||||||
"""
|
"""
|
||||||
Return True if data differs from initial.
|
Return True if data differs from initial.
|
||||||
|
|
|
@ -324,6 +324,15 @@ class BaseModelForm(BaseForm):
|
||||||
self._validate_unique = False
|
self._validate_unique = False
|
||||||
super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
|
super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
|
||||||
error_class, label_suffix, empty_permitted)
|
error_class, label_suffix, empty_permitted)
|
||||||
|
# Apply ``limit_choices_to`` to each field.
|
||||||
|
for field_name in self.fields:
|
||||||
|
formfield = self.fields[field_name]
|
||||||
|
if hasattr(formfield, 'queryset'):
|
||||||
|
limit_choices_to = formfield.limit_choices_to
|
||||||
|
if limit_choices_to is not None:
|
||||||
|
if callable(limit_choices_to):
|
||||||
|
limit_choices_to = limit_choices_to()
|
||||||
|
formfield.queryset = formfield.queryset.complex_filter(limit_choices_to)
|
||||||
|
|
||||||
def _get_validation_exclusions(self):
|
def _get_validation_exclusions(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1082,7 +1091,8 @@ class ModelChoiceField(ChoiceField):
|
||||||
|
|
||||||
def __init__(self, queryset, empty_label="---------", cache_choices=False,
|
def __init__(self, queryset, empty_label="---------", cache_choices=False,
|
||||||
required=True, widget=None, label=None, initial=None,
|
required=True, widget=None, label=None, initial=None,
|
||||||
help_text='', to_field_name=None, *args, **kwargs):
|
help_text='', to_field_name=None, limit_choices_to=None,
|
||||||
|
*args, **kwargs):
|
||||||
if required and (initial is not None):
|
if required and (initial is not None):
|
||||||
self.empty_label = None
|
self.empty_label = None
|
||||||
else:
|
else:
|
||||||
|
@ -1094,6 +1104,7 @@ class ModelChoiceField(ChoiceField):
|
||||||
Field.__init__(self, required, widget, label, initial, help_text,
|
Field.__init__(self, required, widget, label, initial, help_text,
|
||||||
*args, **kwargs)
|
*args, **kwargs)
|
||||||
self.queryset = queryset
|
self.queryset = queryset
|
||||||
|
self.limit_choices_to = limit_choices_to # limit the queryset later.
|
||||||
self.choice_cache = None
|
self.choice_cache = None
|
||||||
self.to_field_name = to_field_name
|
self.to_field_name = to_field_name
|
||||||
|
|
||||||
|
|
|
@ -1078,21 +1078,45 @@ define the details of how the relation works.
|
||||||
|
|
||||||
.. attribute:: ForeignKey.limit_choices_to
|
.. attribute:: ForeignKey.limit_choices_to
|
||||||
|
|
||||||
A dictionary of lookup arguments and values (see :doc:`/topics/db/queries`)
|
Sets a limit to the available choices for this field when this field is
|
||||||
that limit the available admin or :class:`ModelForm <django.forms.ModelForm>`
|
rendered using a ``ModelForm`` or the admin (by default, all objects
|
||||||
choices for this object. For example::
|
in the queryset are available to choose). Either a dictionary, a
|
||||||
|
:class:`~django.db.models.Q` object, or a callable returning a
|
||||||
|
dictionary or :class:`~django.db.models.Q` object can be used.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
staff_member = models.ForeignKey(User, limit_choices_to={'is_staff': True})
|
staff_member = models.ForeignKey(User, limit_choices_to={'is_staff': True})
|
||||||
|
|
||||||
causes the corresponding field on the ``ModelForm`` to list only ``Users``
|
causes the corresponding field on the ``ModelForm`` to list only ``Users``
|
||||||
that have ``is_staff=True``.
|
that have ``is_staff=True``. This may be helpful in the Django admin.
|
||||||
|
|
||||||
Instead of a dictionary this can also be a :class:`Q object
|
The callable form can be helpful, for instance, when used in conjunction
|
||||||
<django.db.models.Q>` for more :ref:`complex queries
|
with the Python ``datetime`` module to limit selections by date range. For
|
||||||
<complex-lookups-with-q>`. However, if ``limit_choices_to`` is a :class:`Q
|
example::
|
||||||
object <django.db.models.Q>` then it will only have an effect on the
|
|
||||||
choices available in the admin when the field is not listed in
|
limit_choices_to = lambda: {'pub_date__lte': datetime.date.utcnow()}
|
||||||
``raw_id_fields`` in the ``ModelAdmin`` for the model.
|
|
||||||
|
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
|
||||||
|
:attr:`~django.contrib.admin.ModelAdmin.raw_id_fields` in the
|
||||||
|
``ModelAdmin`` for the model.
|
||||||
|
|
||||||
|
.. 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, 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
|
.. attribute:: ForeignKey.related_name
|
||||||
|
|
||||||
|
|
|
@ -608,6 +608,10 @@ Models
|
||||||
* It is now possible to use ``None`` as a query value for the :lookup:`iexact`
|
* It is now possible to use ``None`` as a query value for the :lookup:`iexact`
|
||||||
lookup.
|
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
|
Signals
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ from .models import (Article, Chapter, Child, Parent, Picture, Widget,
|
||||||
UnchangeableObject, UserMessenger, Simple, Choice, ShortMessage, Telegram,
|
UnchangeableObject, UserMessenger, Simple, Choice, ShortMessage, Telegram,
|
||||||
FilteredManager, EmptyModelHidden, EmptyModelVisible, EmptyModelMixin,
|
FilteredManager, EmptyModelHidden, EmptyModelVisible, EmptyModelMixin,
|
||||||
State, City, Restaurant, Worker, ParentWithDependentChildren,
|
State, City, Restaurant, Worker, ParentWithDependentChildren,
|
||||||
DependentChild)
|
DependentChild, StumpJoke)
|
||||||
|
|
||||||
|
|
||||||
def callable_year(dt_value):
|
def callable_year(dt_value):
|
||||||
|
@ -884,6 +884,7 @@ site.register(ParentWithDependentChildren, ParentWithDependentChildrenAdmin)
|
||||||
site.register(EmptyModelHidden, EmptyModelHiddenAdmin)
|
site.register(EmptyModelHidden, EmptyModelHiddenAdmin)
|
||||||
site.register(EmptyModelVisible, EmptyModelVisibleAdmin)
|
site.register(EmptyModelVisible, EmptyModelVisibleAdmin)
|
||||||
site.register(EmptyModelMixin, EmptyModelMixinAdmin)
|
site.register(EmptyModelMixin, EmptyModelMixinAdmin)
|
||||||
|
site.register(StumpJoke)
|
||||||
|
|
||||||
# Register core models we need in our tests
|
# Register core models we need in our tests
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
|
|
|
@ -173,6 +173,33 @@ class Sketch(models.Model):
|
||||||
return self.title
|
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):
|
class Fabric(models.Model):
|
||||||
NG_CHOICES = (
|
NG_CHOICES = (
|
||||||
('Textured', (
|
('Textured', (
|
||||||
|
|
|
@ -52,7 +52,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount,
|
||||||
Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject,
|
Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject,
|
||||||
Simple, UndeletableObject, UnchangeableObject, Choice, ShortMessage,
|
Simple, UndeletableObject, UnchangeableObject, Choice, ShortMessage,
|
||||||
Telegram, Pizza, Topping, FilteredManager, City, Restaurant, Worker,
|
Telegram, Pizza, Topping, FilteredManager, City, Restaurant, Worker,
|
||||||
ParentWithDependentChildren)
|
ParentWithDependentChildren, Character)
|
||||||
from .admin import site, site2, CityAdmin
|
from .admin import site, site2, CityAdmin
|
||||||
|
|
||||||
|
|
||||||
|
@ -3661,6 +3661,33 @@ class ReadonlyTest(TestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
threepwood = Character.objects.create(
|
||||||
|
username='threepwood',
|
||||||
|
last_action=datetime.datetime.today() + datetime.timedelta(days=1),
|
||||||
|
)
|
||||||
|
marley = Character.objects.create(
|
||||||
|
username='marley',
|
||||||
|
last_action=datetime.datetime.today() - datetime.timedelta(days=1),
|
||||||
|
)
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/stumpjoke/add/')
|
||||||
|
# The allowed option should appear twice; the limited option should not appear.
|
||||||
|
self.assertContains(response, threepwood.username, count=2)
|
||||||
|
self.assertNotContains(response, marley.username)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
|
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
|
||||||
class RawIdFieldsTest(TestCase):
|
class RawIdFieldsTest(TestCase):
|
||||||
urls = "admin_views.urls"
|
urls = "admin_views.urls"
|
||||||
|
|
|
@ -8,6 +8,7 @@ words, most of these tests should be rewritten.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
@ -71,7 +72,6 @@ class Article(models.Model):
|
||||||
status = models.PositiveIntegerField(choices=ARTICLE_STATUS, blank=True, null=True)
|
status = models.PositiveIntegerField(choices=ARTICLE_STATUS, blank=True, null=True)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
import datetime
|
|
||||||
if not self.id:
|
if not self.id:
|
||||||
self.created = datetime.date.today()
|
self.created = datetime.date.today()
|
||||||
return super(Article, self).save()
|
return super(Article, self).save()
|
||||||
|
@ -329,3 +329,21 @@ class CustomErrorMessage(models.Model):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.name1 == 'FORBIDDEN_VALUE':
|
if self.name1 == 'FORBIDDEN_VALUE':
|
||||||
raise ValidationError({'name1': [ValidationError('Model.clean() error messages.')]})
|
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="+")
|
||||||
|
|
|
@ -22,7 +22,8 @@ from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book,
|
||||||
DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle,
|
DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle,
|
||||||
ImprovedArticleWithParentLink, Inventory, Post, Price,
|
ImprovedArticleWithParentLink, Inventory, Post, Price,
|
||||||
Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem,
|
Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem,
|
||||||
ArticleStatusNote, DateTimePost, CustomErrorMessage, test_images)
|
ArticleStatusNote, DateTimePost, CustomErrorMessage, test_images,
|
||||||
|
StumpJoke, Character)
|
||||||
|
|
||||||
if test_images:
|
if test_images:
|
||||||
from .models import ImageFile, OptionalImageFile
|
from .models import ImageFile, OptionalImageFile
|
||||||
|
@ -521,6 +522,12 @@ class FieldOverridesTroughFormMetaForm(forms.ModelForm):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StumpJokeForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = StumpJoke
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class TestFieldOverridesTroughFormMeta(TestCase):
|
class TestFieldOverridesTroughFormMeta(TestCase):
|
||||||
def test_widget_overrides(self):
|
def test_widget_overrides(self):
|
||||||
form = FieldOverridesTroughFormMetaForm()
|
form = FieldOverridesTroughFormMetaForm()
|
||||||
|
@ -1921,3 +1928,34 @@ class ModelFormInheritanceTests(TestCase):
|
||||||
self.assertEqual(list(type(str('NewForm'), (ModelForm, Mixin, Form), {})().fields.keys()), ['name'])
|
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, Mixin), {})().fields.keys()), ['name', 'age'])
|
||||||
self.assertEqual(list(type(str('NewForm'), (ModelForm, Form), {'age': None})().fields.keys()), ['name'])
|
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.threepwood = Character.objects.create(
|
||||||
|
username='threepwood',
|
||||||
|
last_action=datetime.datetime.today() + datetime.timedelta(days=1),
|
||||||
|
)
|
||||||
|
self.marley = Character.objects.create(
|
||||||
|
username='marley',
|
||||||
|
last_action=datetime.datetime.today() - datetime.timedelta(days=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
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.threepwood, stumpjokeform.fields['most_recently_fooled'].queryset)
|
||||||
|
self.assertNotIn(self.marley, 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.threepwood, stumpjokeform.fields['has_fooled_today'].queryset)
|
||||||
|
self.assertNotIn(self.marley, stumpjokeform.fields['has_fooled_today'].queryset)
|
||||||
|
|
Loading…
Reference in New Issue