Index: docs/ref/contrib/admin/index.txt
===================================================================
--- docs/ref/contrib/admin/index.txt	(revision 16337)
+++ docs/ref/contrib/admin/index.txt	(working copy)
@@ -967,6 +967,15 @@
     a ``dictionary``, as described above in the :attr:`ModelAdmin.prepopulated_fields`
     section.
 
+.. method:: ModelAdmin.get_list_display(self, request)
+
+    .. versionadded:: 1.4
+
+    The ``get_list_display`` method is given the ``HttpRequest`` and is
+    expected to return a ``list`` or ``tuple`` of field names that will be
+    displayed on the changelist view as described above in the
+    :attr:`ModelAdmin.list_display` section.
+
 .. method:: ModelAdmin.get_urls(self)
 
     The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(revision 16337)
+++ django/contrib/admin/options.py	(working copy)
@@ -625,6 +625,13 @@
             description = capfirst(action.replace('_', ' '))
         return func, action, description
 
+    def get_list_display(self, request):
+        """
+        Return a sequence containing the fields to be displayed on the
+        changelist.
+        """
+        return self.list_display
+
     def construct_change_message(self, request, form, formsets):
         """
         Construct a change message from a changed object.
@@ -1053,7 +1060,7 @@
         actions = self.get_actions(request)
 
         # Remove action checkboxes if there aren't any actions available.
-        list_display = list(self.list_display)
+        list_display = list(self.get_list_display(request))
         if not actions:
             try:
                 list_display.remove('action_checkbox')
Index: tests/regressiontests/admin_changelist/tests.py
===================================================================
--- tests/regressiontests/admin_changelist/tests.py	(revision 16337)
+++ tests/regressiontests/admin_changelist/tests.py	(working copy)
@@ -4,19 +4,25 @@
 from django.core.paginator import Paginator
 from django.template import Context, Template
 from django.test import TransactionTestCase
+from django.test.client import RequestFactory
+from django.contrib.auth.models import User
 
 from models import (Child, Parent, Genre, Band, Musician, Group, Quartet,
     Membership, ChordsMusician, ChordsBand, Invitation)
 
 
 class ChangeListTests(TransactionTestCase):
+    def setUp(self):
+        self.factory = RequestFactory()
+
     def test_select_related_preserved(self):
         """
         Regression test for #10348: ChangeList.get_query_set() shouldn't
         overwrite a custom select_related provided by ModelAdmin.queryset().
         """
         m = ChildAdmin(Child, admin.site)
-        cl = ChangeList(MockRequest(), Child, m.list_display, m.list_display_links,
+        request = self.factory.get('/child/')
+        cl = ChangeList(request, Child, m.list_display, m.list_display_links,
                 m.list_filter, m.date_hierarchy, m.search_fields,
                 m.list_select_related, m.list_per_page, m.list_editable, m)
         self.assertEqual(cl.query_set.query.select_related, {'parent': {'name': {}}})
@@ -27,7 +33,7 @@
         for relationship fields
         """
         new_child = Child.objects.create(name='name', parent=None)
-        request = MockRequest()
+        request = self.factory.get('/child/')
         m = ChildAdmin(Child, admin.site)
         cl = ChangeList(request, Child, m.list_display, m.list_display_links,
                 m.list_filter, m.date_hierarchy, m.search_fields,
@@ -40,7 +46,6 @@
         self.assertFalse(table_output.find(row_html) == -1,
             'Failed to find expected row element: %s' % table_output)
 
-
     def test_result_list_html(self):
         """
         Verifies that inclusion tag result_list generates a table when with
@@ -48,7 +53,7 @@
         """
         new_parent = Parent.objects.create(name='parent')
         new_child = Child.objects.create(name='name', parent=new_parent)
-        request = MockRequest()
+        request = self.factory.get('/child/')
         m = ChildAdmin(Child, admin.site)
         cl = ChangeList(request, Child, m.list_display, m.list_display_links,
                 m.list_filter, m.date_hierarchy, m.search_fields,
@@ -72,7 +77,7 @@
         """
         new_parent = Parent.objects.create(name='parent')
         new_child = Child.objects.create(name='name', parent=new_parent)
-        request = MockRequest()
+        request = self.factory.get('/child/')
         m = ChildAdmin(Child, admin.site)
 
         # Test with list_editable fields
@@ -104,8 +109,7 @@
         new_parent = Parent.objects.create(name='parent')
         for i in range(200):
             new_child = Child.objects.create(name='name %s' % i, parent=new_parent)
-        request = MockRequest()
-        request.GET['p'] = -1 # Anything outside range
+        request = self.factory.get('/child/', data={'p': -1})  # Anything outside range
         m = ChildAdmin(Child, admin.site)
 
         # Test with list_editable fields
@@ -122,7 +126,7 @@
         for i in range(200):
             new_child = Child.objects.create(name='name %s' % i, parent=new_parent)
 
-        request = MockRequest()
+        request = self.factory.get('/child/')
         m = ChildAdmin(Child, admin.site)
         m.list_display = ['id', 'name', 'parent']
         m.list_display_links = ['id']
@@ -148,7 +152,7 @@
         band.genres.add(blues)
 
         m = BandAdmin(Band, admin.site)
-        request = MockFilterRequest('genres', blues.pk)
+        request = self.factory.get('/band/', data={'genres': blues.pk})
 
         cl = ChangeList(request, Band, m.list_display,
                 m.list_display_links, m.list_filter, m.date_hierarchy,
@@ -171,7 +175,7 @@
         Membership.objects.create(group=band, music=lead, role='bass player')
 
         m = GroupAdmin(Group, admin.site)
-        request = MockFilterRequest('members', lead.pk)
+        request = self.factory.get('/group/', data={'members': lead.pk})
 
         cl = ChangeList(request, Group, m.list_display,
                 m.list_display_links, m.list_filter, m.date_hierarchy,
@@ -195,7 +199,7 @@
         Membership.objects.create(group=four, music=lead, role='guitar player')
 
         m = QuartetAdmin(Quartet, admin.site)
-        request = MockFilterRequest('members', lead.pk)
+        request = self.factory.get('/quartet/', data={'members': lead.pk})
 
         cl = ChangeList(request, Quartet, m.list_display,
                 m.list_display_links, m.list_filter, m.date_hierarchy,
@@ -219,7 +223,7 @@
         Invitation.objects.create(band=three, player=lead, instrument='bass')
 
         m = ChordsBandAdmin(ChordsBand, admin.site)
-        request = MockFilterRequest('members', lead.pk)
+        request = self.factory.get('/chordsband/', data={'members': lead.pk})
 
         cl = ChangeList(request, ChordsBand, m.list_display,
                 m.list_display_links, m.list_filter, m.date_hierarchy,
@@ -242,7 +246,7 @@
         Child.objects.create(parent=parent, name='Daniel')
 
         m = ParentAdmin(Parent, admin.site)
-        request = MockFilterRequest('child__name', 'Daniel')
+        request = self.factory.get('/parent/', data={'child__name': 'Daniel'})
 
         cl = ChangeList(request, Parent, m.list_display, m.list_display_links,
                         m.list_filter, m.date_hierarchy, m.search_fields,
@@ -262,7 +266,7 @@
         Child.objects.create(parent=parent, name='Daniel')
 
         m = ParentAdmin(Parent, admin.site)
-        request = MockSearchRequest('daniel')
+        request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel'})
 
         cl = ChangeList(request, Parent, m.list_display, m.list_display_links,
                         m.list_filter, m.date_hierarchy, m.search_fields,
@@ -282,7 +286,7 @@
             Child.objects.create(name='name %s' % i, parent=parent)
             Child.objects.create(name='filtered %s' % i, parent=parent)
 
-        request = MockRequest()
+        request = self.factory.get('/child/')
 
         # Test default queryset
         m = ChildAdmin(Child, admin.site)
@@ -302,7 +306,52 @@
         self.assertEqual(cl.paginator.count, 30)
         self.assertEqual(cl.paginator.page_range, [1, 2, 3])
 
+    def test_dynamic_list_display(self):
+        """
+        Regression tests for #14206: dynamic list_display support.
+        """
+        parent = Parent.objects.create(name='parent')
+        for i in range(10):
+            Child.objects.create(name='child %s' % i, parent=parent)
 
+        user_noparents = User.objects.create(
+            username='noparents',
+            is_superuser=True)
+        user_parents = User.objects.create(
+            username='parents',
+            is_superuser=True)
+
+        def _mocked_authenticated_request(user):
+            request = self.factory.get('/child/')
+            request.user = user
+            return request
+
+        # Test with user 'noparents'
+        m = DynamicListDisplayChildAdmin(Child, admin.site)
+        request = _mocked_authenticated_request(user_noparents)
+        response = m.changelist_view(request)
+        # XXX - Calling render here to avoid ContentNotRenderedError to be
+        # raised. Ticket #15826 should fix this but it's not yet integrated.
+        response.render()
+        self.assertNotContains(response, 'Parent object')
+
+        # Test with user 'parents'
+        m = DynamicListDisplayChildAdmin(Child, admin.site)
+        request = _mocked_authenticated_request(user_parents)
+        response = m.changelist_view(request)
+        # XXX - #15826
+        response.render()
+        self.assertContains(response, 'Parent object')
+
+        # Test default implementation
+        m = ChildAdmin(Child, admin.site)
+        request = _mocked_authenticated_request(user_noparents)
+        response = m.changelist_view(request)
+        # XXX - #15826
+        response.render()
+        self.assertContains(response, 'Parent object')
+
+
 class ParentAdmin(admin.ModelAdmin):
     list_filter = ['child__name']
     search_fields = ['child__name']
@@ -311,18 +360,19 @@
 class ChildAdmin(admin.ModelAdmin):
     list_display = ['name', 'parent']
     list_per_page = 10
+
     def queryset(self, request):
         return super(ChildAdmin, self).queryset(request).select_related("parent__name")
 
+
 class FilteredChildAdmin(admin.ModelAdmin):
     list_display = ['name', 'parent']
     list_per_page = 10
+
     def queryset(self, request):
         return super(FilteredChildAdmin, self).queryset(request).filter(
             name__contains='filtered')
 
-class MockRequest(object):
-    GET = {}
 
 class CustomPaginator(Paginator):
     def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True):
@@ -333,19 +383,25 @@
 class BandAdmin(admin.ModelAdmin):
     list_filter = ['genres']
 
+
 class GroupAdmin(admin.ModelAdmin):
     list_filter = ['members']
 
+
 class QuartetAdmin(admin.ModelAdmin):
     list_filter = ['members']
 
+
 class ChordsBandAdmin(admin.ModelAdmin):
     list_filter = ['members']
 
-class MockFilterRequest(object):
-    def __init__(self, filter, q):
-        self.GET = {filter: q}
 
-class MockSearchRequest(object):
-    def __init__(self, q):
-        self.GET = {SEARCH_VAR: q}
+class DynamicListDisplayChildAdmin(admin.ModelAdmin):
+    list_display = ('name', 'parent')
+
+    def get_list_display(self, request):
+        my_list_display = list(self.list_display)
+        if request.user.username == 'noparents':
+            my_list_display.remove('parent')
+
+        return my_list_display
