diff options
| author | Christopher Adams <christopher.r.adams@gmail.com> | 2014-02-01 14:23:31 -0500 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2014-02-11 14:05:12 -0500 |
| commit | eefc88feefec0c3685bfb102714530b751b4ae90 (patch) | |
| tree | 3cab4b02c13b76b6355d475d91ca2e157a126b18 /django | |
| parent | a718fcf201b04ba254e9073be82f51ae1ae3a853 (diff) | |
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.
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/admin/options.py | 5 | ||||
| -rw-r--r-- | django/contrib/admin/utils.py | 12 | ||||
| -rw-r--r-- | django/contrib/admin/widgets.py | 5 | ||||
| -rw-r--r-- | django/db/models/fields/__init__.py | 4 | ||||
| -rw-r--r-- | django/db/models/fields/related.py | 38 | ||||
| -rw-r--r-- | django/forms/fields.py | 11 | ||||
| -rw-r--r-- | django/forms/models.py | 13 |
7 files changed, 74 insertions, 14 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index e3c3e8e57e..7b0dd8ebcb 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -240,7 +240,7 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): if related_admin is not None: ordering = related_admin.get_ordering(request) 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 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, # are allowed to work. 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(): if k == lookup and v == value: return True diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index 0b74a6e75f..a2f2e9fa7b 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -459,17 +459,17 @@ def get_limit_choices_to_from_path(model, path): """ Return Q object for limiting choices if applicable. 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 = remove_trailing_data_field(fields) - limit_choices_to = ( + get_limit_choices_to = ( fields and hasattr(fields[-1], 'rel') and - getattr(fields[-1].rel, 'limit_choices_to', None)) - if not limit_choices_to: + getattr(fields[-1].rel, 'get_limit_choices_to', None)) + if not get_limit_choices_to: 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 else: return models.Q(**limit_choices_to) # convert dict to Q diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index f527d72f3f..f501ffb1a8 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -180,7 +180,10 @@ class ForeignKeyRawIdWidget(forms.TextInput): return mark_safe(''.join(output)) 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): from django.contrib.admin.views.main import TO_FIELD_VAR diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 77a48d0723..e2fef7fb71 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -742,11 +742,11 @@ class Field(RegisterLookupMixin): lst = [(getattr(x, self.rel.get_related_field().attname), smart_text(x)) for x in rel_model._default_manager.complex_filter( - self.rel.limit_choices_to)] + self.get_limit_choices_to())] else: lst = [(x._get_pk_val(), smart_text(x)) for x in rel_model._default_manager.complex_filter( - self.rel.limit_choices_to)] + self.get_limit_choices_to())] return first_choice + lst def get_choices_default(self): diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 6bd91817dd..f490066516 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -309,6 +309,35 @@ class RelatedField(Field): if not cls._meta.abstract: 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): # This method defines the name that can be used to identify this # 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. if not self.rel.is_hidden() and not related.model._meta.swapped: 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: 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( **{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(): raise exceptions.ValidationError( self.error_messages['invalid'], @@ -1691,7 +1723,7 @@ class ForeignKey(ForeignObject): (self.name, self.rel.to)) defaults = { '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, } defaults.update(kwargs) @@ -2127,7 +2159,7 @@ class ManyToManyField(RelatedField): db = kwargs.pop('using', None) defaults = { '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) # If initial is passed in, it's a list of related objects, but the diff --git a/django/forms/fields.py b/django/forms/fields.py index 1ce36c199c..629aa69c5d 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -170,6 +170,17 @@ class Field(object): """ 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): """ Return True if data differs from initial. diff --git a/django/forms/models.py b/django/forms/models.py index 37a1b93bf5..a0b47e64b4 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -324,6 +324,15 @@ class BaseModelForm(BaseForm): self._validate_unique = False super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data, 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): """ @@ -1082,7 +1091,8 @@ class ModelChoiceField(ChoiceField): def __init__(self, queryset, empty_label="---------", cache_choices=False, 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): self.empty_label = None else: @@ -1094,6 +1104,7 @@ class ModelChoiceField(ChoiceField): Field.__init__(self, required, widget, label, initial, help_text, *args, **kwargs) self.queryset = queryset + self.limit_choices_to = limit_choices_to # limit the queryset later. self.choice_cache = None self.to_field_name = to_field_name |
