diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 3a0ad74..a1bc2f2 100644
a
|
b
|
from django.core.urlresolvers import reverse
|
15 | 15 | from django.db import models, transaction, router |
16 | 16 | from django.db.models.related import RelatedObject |
17 | 17 | from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist |
| 18 | from django.db.models.fields.related import ManyRelatedObjectsDescriptor |
18 | 19 | from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS |
19 | 20 | from django.http import Http404, HttpResponse, HttpResponseRedirect |
20 | 21 | from django.shortcuts import get_object_or_404 |
… |
… |
class ModelAdmin(BaseModelAdmin):
|
455 | 456 | "exclude": exclude, |
456 | 457 | "formfield_callback": partial(self.formfield_for_dbfield, request=request), |
457 | 458 | } |
| 459 | if fields: |
| 460 | revM2M_widgets = {} |
| 461 | other_fields = set(fields) - set(self.model._meta.fields + self.model._meta.many_to_many) |
| 462 | for item in other_fields: |
| 463 | if hasattr(self.model, item) and isinstance(getattr(self.model, item), ManyRelatedObjectsDescriptor): |
| 464 | if item in (list(self.filter_vertical) + list(self.filter_horizontal)): |
| 465 | revM2M_widgets[item] = widgets.FilteredSelectMultiple(item, (item in self.filter_vertical)) |
| 466 | if widgets: |
| 467 | defaults['widgets'] = revM2M_widgets |
458 | 468 | defaults.update(kwargs) |
459 | 469 | return modelform_factory(self.model, **defaults) |
460 | 470 | |
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 733f89d..0787549 100644
a
|
b
|
|
1 | 1 | from django.core.exceptions import ImproperlyConfigured |
2 | 2 | from django.db import models |
3 | 3 | from django.db.models.fields import FieldDoesNotExist |
| 4 | from django.db.models.fields.related import ManyRelatedObjectsDescriptor |
4 | 5 | from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model, |
5 | 6 | _get_foreign_key) |
6 | 7 | from django.contrib.admin import ListFilter, FieldListFilter |
… |
… |
def validate_fields_spec(cls, model, opts, flds, label):
|
246 | 247 | # readonly_fields will handle the validation of such |
247 | 248 | # things. |
248 | 249 | continue |
| 250 | if hasattr(model, field) and isinstance(getattr(model, field), ManyRelatedObjectsDescriptor): |
| 251 | # allow reverse M2M descriptors |
| 252 | continue |
249 | 253 | check_formfield(cls, model, opts, label, field) |
250 | 254 | try: |
251 | 255 | f = opts.get_field(field) |
… |
… |
def validate_base(cls, model):
|
331 | 335 | if hasattr(cls, 'filter_horizontal'): |
332 | 336 | check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) |
333 | 337 | for idx, field in enumerate(cls.filter_horizontal): |
334 | | f = get_field(cls, model, opts, 'filter_horizontal', field) |
335 | | if not isinstance(f, models.ManyToManyField): |
| 338 | valid = False |
| 339 | if hasattr(model, field) and isinstance(getattr(model, field), ManyRelatedObjectsDescriptor): |
| 340 | valid = True |
| 341 | if not valid: |
| 342 | f = get_field(cls, model, opts, 'filter_horizontal', field) |
| 343 | if isinstance(f, models.ManyToManyField): |
| 344 | valid = True |
| 345 | if not valid: |
336 | 346 | raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " |
337 | 347 | "a ManyToManyField." % (cls.__name__, idx)) |
338 | 348 | |
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
index 3868794..a58069c 100644
a
|
b
|
csrf_protect_m = method_decorator(csrf_protect)
|
19 | 19 | class GroupAdmin(admin.ModelAdmin): |
20 | 20 | search_fields = ('name',) |
21 | 21 | ordering = ('name',) |
22 | | filter_horizontal = ('permissions',) |
| 22 | fields = ('name', 'permissions', 'user_set') |
| 23 | filter_horizontal = ('permissions', 'user_set') |
23 | 24 | |
24 | 25 | def formfield_for_manytomany(self, db_field, request=None, **kwargs): |
25 | 26 | if db_field.name == 'permissions': |
diff --git a/django/forms/models.py b/django/forms/models.py
index b65f067..d6cc94a 100644
a
|
b
|
from django.utils.datastructures import SortedDict
|
18 | 18 | from django.utils.text import get_text_list, capfirst |
19 | 19 | from django.utils.translation import ugettext_lazy as _, ugettext |
20 | 20 | |
21 | | |
22 | 21 | __all__ = ( |
23 | 22 | 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', |
24 | 23 | 'save_instance', 'ModelChoiceField', 'ModelMultipleChoiceField', |
… |
… |
def save_instance(form, instance, fields=None, fail_message='saved',
|
81 | 80 | continue |
82 | 81 | if f.name in cleaned_data: |
83 | 82 | f.save_form_data(instance, cleaned_data[f.name]) |
| 83 | if fields: |
| 84 | other_fields = set(fields) - set(opts.fields + opts.many_to_many) |
| 85 | for item in other_fields: |
| 86 | setattr(instance, item, cleaned_data[item]) |
| 87 | |
84 | 88 | if commit: |
85 | 89 | # If we are committing, save the instance and the m2m data immediately. |
86 | 90 | instance.save() |
… |
… |
def model_to_dict(instance, fields=None, exclude=None):
|
107 | 111 | the ``fields`` argument. |
108 | 112 | """ |
109 | 113 | # avoid a circular import |
110 | | from django.db.models.fields.related import ManyToManyField |
| 114 | from django.db.models.fields.related import ManyToManyField, ManyRelatedObjectsDescriptor |
111 | 115 | opts = instance._meta |
112 | 116 | data = {} |
113 | 117 | for f in opts.fields + opts.many_to_many: |
… |
… |
def model_to_dict(instance, fields=None, exclude=None):
|
128 | 132 | data[f.name] = [obj.pk for obj in f.value_from_object(instance)] |
129 | 133 | else: |
130 | 134 | data[f.name] = f.value_from_object(instance) |
| 135 | if fields: |
| 136 | other_fields = set(fields) - set(data.keys()) |
| 137 | for item in other_fields: |
| 138 | if hasattr(instance.__class__, item) and \ |
| 139 | isinstance(getattr(instance.__class__, item), ManyRelatedObjectsDescriptor): |
| 140 | if instance.pk is None: |
| 141 | data[item] = [] |
| 142 | else: |
| 143 | # MultipleChoiceWidget needs a list of pks, not object instances. |
| 144 | data[item] = [obj.pk for obj in getattr(instance, item).all()] |
131 | 145 | return data |
132 | 146 | |
133 | 147 | def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None): |
… |
… |
def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
|
140 | 154 | ``exclude`` is an optional list of field names. If provided, the named |
141 | 155 | fields will be excluded from the returned fields, even if they are listed |
142 | 156 | in the ``fields`` argument. |
143 | | """ |
| 157 | """ |
| 158 | from django.db.models.fields.related import ManyRelatedObjectsDescriptor |
| 159 | from django.forms import ModelMultipleChoiceField |
| 160 | |
144 | 161 | field_list = [] |
145 | 162 | ignored = [] |
146 | 163 | opts = model._meta |
… |
… |
def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
|
167 | 184 | field_list.append((f.name, formfield)) |
168 | 185 | else: |
169 | 186 | ignored.append(f.name) |
| 187 | |
| 188 | if fields: |
| 189 | missing_fields = set(fields) - set([k[0] for k in field_list]) |
| 190 | for item in missing_fields: |
| 191 | if hasattr(model, item) and isinstance(getattr(model, item), ManyRelatedObjectsDescriptor): |
| 192 | kwargs = { |
| 193 | 'required': False, |
| 194 | 'queryset': getattr(model, item).related.model._default_manager.all() |
| 195 | } |
| 196 | if widgets and item in widgets: |
| 197 | kwargs['widget'] = widgets[item] |
| 198 | formfield = ModelMultipleChoiceField(**kwargs) |
| 199 | field_list.append((item, formfield)) |
| 200 | |
170 | 201 | field_dict = SortedDict(field_list) |
171 | 202 | if fields: |
172 | 203 | field_dict = SortedDict( |
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
index f4ec63f..42aebdb 100644
a
|
b
|
class GroupAdminTest(TestCase):
|
2986 | 2986 | def test_group_permission_performance(self): |
2987 | 2987 | g = Group.objects.create(name="test_group") |
2988 | 2988 | |
2989 | | with self.assertNumQueries(6): # instead of 259! |
| 2989 | with self.assertNumQueries(8): # instead of 259! |
2990 | 2990 | response = self.client.get('/test_admin/admin/auth/group/%s/' % g.pk) |
2991 | 2991 | self.assertEqual(response.status_code, 200) |
2992 | 2992 | |