Index: django/contrib/admin/validation.py
===================================================================
--- django/contrib/admin/validation.py	(revision 9728)
+++ django/contrib/admin/validation.py	(working copy)
@@ -5,9 +5,10 @@
 
 from django.core.exceptions import ImproperlyConfigured
 from django.db import models
 from django.forms.models import BaseModelForm, BaseModelFormSet, fields_for_model
 from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
 from django.contrib.admin.options import HORIZONTAL, VERTICAL
+from django.contrib.admin.views.main import ChangeList
 
 __all__ = ['validate']
 
@@ -117,9 +118,14 @@
                 raise ImproperlyConfigured("'%s.inlines[%d].model' does not "
                         "inherit from models.Model." % (cls.__name__, idx))
             validate_base(inline, inline.model)
             validate_inline(inline)
 
+    # changelist_class = ChangeList
+    if hasattr(cls, 'changelist_class') and not issubclass(cls.changelist_class, ChangeList):
+        raise ImproperlyConfigured("'%s.changelist_class' does not inherit "
+                        "from admin.views.main.ChangeList." % cls.__name__)
+

 def validate_inline(cls):
     # model is already verified to exist and be a Model
     if cls.fk_name: # default value is None
         f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name)
 
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(revision 9728)
+++ django/contrib/admin/options.py	(working copy)
@@ -153,6 +153,9 @@
 
 class ModelAdmin(BaseModelAdmin):
     "Encapsulates all admin options and functionality for a given model."
+    # Avoid circular import of ChangeList
+    from django.contrib.admin.views.main import ChangeList
+    
     __metaclass__ = forms.MediaDefiningClass
 
     list_display = ('__str__',)
@@ -166,6 +169,7 @@
     save_on_top = False
     ordering = None
     inlines = []
+    changelist_class = ChangeList
 
     # Custom templates (designed to be over-ridden in subclasses)
     change_form_template = None
@@ -620,13 +624,12 @@
 
     def changelist_view(self, request, extra_context=None):
         "The 'change list' admin view for this model."
-        from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
         opts = self.model._meta
         app_label = opts.app_label
         if not self.has_change_permission(request, None):
             raise PermissionDenied
         try:
-            cl = ChangeList(request, self.model, self.list_display, self.list_display_links, self.list_filter,
+            cl = self.changelist_class(request, self.model, self.list_display, self.list_display_links, self.list_filter,
                 self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self)
         except IncorrectLookupParameters:
             # Wacky lookup parameters were given, so redirect to the main
@@ -634,6 +637,7 @@
             # parameter via the query string. If wacky parameters were given and
             # the 'invalid=1' parameter was already in the query string, something
             # is screwed up with the database, so display an error page.
+            from django.contrib.admin.views.main import ERROR_FLAG
             if ERROR_FLAG in request.GET.keys():
                 return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
             return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
Index: tests/regressiontests/admin_views/tests.py
===================================================================
--- tests/regressiontests/admin_views/tests.py	(revision 9728)
+++ tests/regressiontests/admin_views/tests.py	(working copy)
@@ -735,3 +735,26 @@
         self.failUnlessEqual(response.status_code, 200)
         response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict)
         self.assertRedirects(response, '/test_admin/admin/admin_views/book/')
+
+class AdminCustomChangeListTest(TestCase):
+    fixtures = ['admin-views-users.xml']
+
+    def setUp(self):
+        self.client.login(username='super', password='secret')
+
+    def tearDown(self):
+        self.client.logout()
+
+    def testCustomLinkPresence(self):
+        """
+        A test to ensure that custom ChangeList class works as expected.
+        """
+        post_data = {
+            "title": u"Lion",
+        }
+        response = self.client.post('/test_admin/admin/admin_views/animal/add/', post_data)
+        self.failUnlessEqual(response.status_code, 302) # redirect somewhere
+        response = self.client.get('/test_admin/admin/admin_views/animal/')
+        self.failUnlessEqual(response.status_code, 200)
+        should_contain = """<a href="custom/1/">Lion</a>"""
+        self.assertContains(response, should_contain)
Index: tests/regressiontests/admin_views/models.py
===================================================================
--- tests/regressiontests/admin_views/models.py	(revision 9728)
+++ tests/regressiontests/admin_views/models.py	(working copy)
@@ -134,12 +134,25 @@
 class ThingAdmin(admin.ModelAdmin):
     list_filter = ('color',)
 
+class Animal(models.Model):
+    title = models.CharField(max_length=20)
+    def __unicode__(self):
+        return self.title
+
+class AnimalChangeList(admin.views.main.ChangeList):
+    def url_for_result(self, result):
+        return "custom/%s/" % admin.util.quote(getattr(result, self.pk_attname))
+
+class AnimalAdmin(admin.ModelAdmin):
+    changelist_class = AnimalChangeList
+
 admin.site.register(Article, ArticleAdmin)
 admin.site.register(CustomArticle, CustomArticleAdmin)
 admin.site.register(Section, inlines=[ArticleInline])
 admin.site.register(ModelWithStringPrimaryKey)
 admin.site.register(Color)
 admin.site.register(Thing, ThingAdmin)
+admin.site.register(Animal, AnimalAdmin)
 
 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
 # That way we cover all four cases:

Index: tests/regressiontests/modeladmin/models.py
===================================================================
--- tests/regressiontests/modeladmin/models.py	(revision 9728)
+++ tests/regressiontests/modeladmin/models.py	(working copy)
@@ -912,5 +912,22 @@
 ...     inlines = [ValidationTestInline]
 >>> validate(ValidationTestModelAdmin, ValidationTestModel)
 
+# changelist_class
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+...     changelist_class = object
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: 'ValidationTestModelAdmin.changelist_class' does not inherit from admin.views.main.ChangeList.
+
+>>> from django.contrib.admin.views.main import ChangeList
+>>> class ValidationTestChangeList(ChangeList):
+...     def url_for_result(self, result):
+...         return "custom/%s/" % quote(getattr(result, self.pk_attname))
+>>> class ValidationTestModelAdmin(ModelAdmin):
+...     changelist_class = ValidationTestChangeList
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
 """
 }
Index: docs/ref/contrib/admin.txt
===================================================================
--- docs/ref/contrib/admin.txt	(revision 9728)
+++ docs/ref/contrib/admin.txt	(working copy)
@@ -77,6 +77,26 @@
     class AuthorAdmin(admin.ModelAdmin):
         date_hierarchy = 'pub_date'
 
+``changelist_class``
+~~~~~~~~~~~~~~~~~~
+
+Set ``changelist_class`` to a class which inherits from 
+``admin.views.main.ChangeList``, and the change list page will use this class
+to render the list.
+
+Example::
+
+    from django.contrib import admin
+    
+    class CustomChangeList(admin.views.main.ChangeList):
+        def url_for_result(self, result):
+            return "custom/%s/" % admin.util.quote(getattr(result, self.pk_attname))
+    
+    class AuthorAdmin(admin.ModelAdmin):
+        date_hierarchy = 'pub_date'
+        changelist_class = CustomChangeList
+    
+
 ``date_hierarchy``
 ~~~~~~~~~~~~~~~~~~
 
