From bf91be83d56d70181de807a5e8d22679cf63a52f Mon Sep 17 00:00:00 2001 From: Zach Borboa Date: Thu, 29 Sep 2016 02:55:10 -0700 Subject: [PATCH] Fixed #24941 -- Added ModelAdmin.get_exclude(). Thanks Ola Sitarska for the initial patch. --- django/contrib/admin/options.py | 22 +++++---- docs/ref/contrib/admin/index.txt | 8 ++++ docs/releases/1.11.txt | 4 ++ tests/modeladmin/tests.py | 81 ++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 0804697594..499d27d02e 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -282,6 +282,12 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): except AttributeError: return mark_safe(self.admin_site.empty_value_display) + def get_exclude(self, request, obj=None): + """ + Hook for specifying exclude. + """ + return self.exclude + def get_fields(self, request, obj=None): """ Hook for specifying fields. @@ -605,13 +611,11 @@ class ModelAdmin(BaseModelAdmin): fields = kwargs.pop('fields') else: fields = flatten_fieldsets(self.get_fieldsets(request, obj)) - if self.exclude is None: - exclude = [] - else: - exclude = list(self.exclude) + excluded = self.get_exclude(request, obj) + exclude = [] if excluded is None else list(excluded) readonly_fields = self.get_readonly_fields(request, obj) exclude.extend(readonly_fields) - if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude: + if excluded is None and hasattr(self.form, '_meta') and self.form._meta.exclude: # Take the custom ModelForm's Meta.exclude into account only if the # ModelAdmin doesn't define its own. exclude.extend(self.form._meta.exclude) @@ -1851,12 +1855,10 @@ class InlineModelAdmin(BaseModelAdmin): fields = kwargs.pop('fields') else: fields = flatten_fieldsets(self.get_fieldsets(request, obj)) - if self.exclude is None: - exclude = [] - else: - exclude = list(self.exclude) + excluded = self.get_exclude(request, obj) + exclude = [] if excluded is None else list(excluded) exclude.extend(self.get_readonly_fields(request, obj)) - if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude: + if excluded is None and hasattr(self.form, '_meta') and self.form._meta.exclude: # Take the custom ModelForm's Meta.exclude into account only if the # InlineModelAdmin doesn't define its own. exclude.extend(self.form._meta.exclude) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 845f467b77..4d3ab6dc1f 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1459,6 +1459,14 @@ templates used by the :class:`ModelAdmin` views: names on the changelist that will be linked to the change view, as described in the :attr:`ModelAdmin.list_display_links` section. +.. method:: ModelAdmin.get_exclude(request, obj=None) + + .. versionadded:: 1.11 + + The ``get_exclude`` method is given the ``HttpRequest`` and the ``obj`` + being edited (or ``None`` on an add form) and is expected to return a list + of fields, as described in :attr:`ModelAdmin.exclude`. + .. method:: ModelAdmin.get_fields(request, obj=None) The ``get_fields`` method is given the ``HttpRequest`` and the ``obj`` diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index f5faedd0a9..ff77ebeabb 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -68,6 +68,10 @@ Minor features * :attr:`.ModelAdmin.date_hierarchy` can now reference fields across relations. +* The new :meth:`ModelAdmin.get_exclude() + ` hook allows specifying the + exclude fields based on the request or model instance. + :mod:`django.contrib.admindocs` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index c2250b923b..e2f565ad9d 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -52,6 +52,7 @@ class ModelAdminTests(TestCase): self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'bio', 'sign_date']) self.assertEqual(list(ma.get_fields(request)), ['name', 'bio', 'sign_date']) self.assertEqual(list(ma.get_fields(request, self.band)), ['name', 'bio', 'sign_date']) + self.assertIsNone(ma.get_exclude(request, self.band)) def test_default_fieldsets(self): # fieldsets_add and fieldsets_change should return a special data structure that @@ -279,6 +280,40 @@ class ModelAdminTests(TestCase): ['main_band', 'opening_band', 'day', 'id', 'DELETE'] ) + def test_overriding_get_exclude(self): + class BandAdmin(ModelAdmin): + def get_exclude(self, request, obj=None): + return ['name'] + + self.assertEqual( + list(BandAdmin(Band, self.site).get_form(request).base_fields), + ['bio', 'sign_date'] + ) + + def test_get_exclude_overrides_exclude(self): + class BandAdmin(ModelAdmin): + exclude = ['bio'] + + def get_exclude(self, request, obj=None): + return ['name'] + + self.assertEqual( + list(BandAdmin(Band, self.site).get_form(request).base_fields), + ['bio', 'sign_date'] + ) + + def test_get_exclude_takes_obj(self): + class BandAdmin(ModelAdmin): + def get_exclude(self, request, obj=None): + if obj: + return ['sign_date'] + return ['name'] + + self.assertEqual( + list(BandAdmin(Band, self.site).get_form(request, self.band).base_fields), + ['name', 'bio'] + ) + def test_custom_form_validation(self): # If we specify a form, it should use it allowing custom validation to work # properly. This won't, however, break any of the admin widgets or media. @@ -346,6 +381,52 @@ class ModelAdminTests(TestCase): list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), ['main_band', 'day', 'transport', 'id', 'DELETE']) + def test_formset_overriding_get_exclude_with_form_fields(self): + class AdminConcertForm(forms.ModelForm): + class Meta: + model = Concert + fields = ['main_band', 'opening_band', 'day', 'transport'] + + class ConcertInline(TabularInline): + form = AdminConcertForm + fk_name = 'main_band' + model = Concert + + def get_exclude(self, request, obj=None): + return ['opening_band'] + + class BandAdmin(ModelAdmin): + inlines = [ConcertInline] + + ma = BandAdmin(Band, self.site) + self.assertEqual( + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), + ['main_band', 'day', 'transport', 'id', 'DELETE'] + ) + + def test_formset_overriding_get_exclude_with_form_exclude(self): + class AdminConcertForm(forms.ModelForm): + class Meta: + model = Concert + exclude = ['day'] + + class ConcertInline(TabularInline): + form = AdminConcertForm + fk_name = 'main_band' + model = Concert + + def get_exclude(self, request, obj=None): + return ['opening_band'] + + class BandAdmin(ModelAdmin): + inlines = [ConcertInline] + + ma = BandAdmin(Band, self.site) + self.assertEqual( + list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields), + ['main_band', 'day', 'transport', 'id', 'DELETE'] + ) + def test_queryset_override(self): # If we need to override the queryset of a ModelChoiceField in our custom form # make sure that RelatedFieldWidgetWrapper doesn't mess that up.