Index: django/contrib/comments/admin.py
===================================================================
--- django/contrib/comments/admin.py	(revision 9776)
+++ django/contrib/comments/admin.py	(working copy)
@@ -1,6 +1,7 @@
 from django.contrib import admin
 from django.contrib.comments.models import Comment
 from django.utils.translation import ugettext_lazy as _
+from django.contrib.comments import get_model
 
 class CommentsAdmin(admin.ModelAdmin):
     fieldsets = (
@@ -21,4 +22,5 @@
     ordering = ('-submit_date',)
     search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')
 
-admin.site.register(Comment, CommentsAdmin)
+if get_model() is Comment:
+    admin.site.register(Comment, CommentsAdmin)
Index: django/contrib/comments/__init__.py
===================================================================
--- django/contrib/comments/__init__.py	(revision 9776)
+++ django/contrib/comments/__init__.py	(working copy)
@@ -2,9 +2,6 @@
 from django.core import urlresolvers
 from django.core.exceptions import ImproperlyConfigured
 
-# Attributes required in the top-level app for COMMENTS_APP
-REQUIRED_COMMENTS_APP_ATTRIBUTES = ["get_model", "get_form", "get_form_target"]
-
 def get_comment_app():
     """
     Get the comment app (i.e. "django.contrib.comments") as defined in the settings
@@ -22,13 +19,6 @@
         raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\
                                    "a non-existing package.")
 
-    # Make sure some specific attributes exist inside that package.
-    for attribute in REQUIRED_COMMENTS_APP_ATTRIBUTES:
-        if not hasattr(package, attribute):
-            raise ImproperlyConfigured("The COMMENTS_APP package %r does not "\
-                                       "define the (required) %r function" % \
-                                            (package, attribute))
-
     return package
 
 def get_comment_app_name():
@@ -39,14 +29,20 @@
     return getattr(settings, 'COMMENTS_APP', 'django.contrib.comments')
 
 def get_model():
+    if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_model"):
+        return get_comment_app().get_model()
     from django.contrib.comments.models import Comment
     return Comment
 
 def get_form():
+    if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_form"):
+        return get_comment_app().get_form()
     from django.contrib.comments.forms import CommentForm
     return CommentForm
 
 def get_form_target():
+    if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_form_target"):
+        return get_comment_app().get_form_target()
     return urlresolvers.reverse("django.contrib.comments.views.comments.post_comment")
 
 def get_flag_url(comment):
@@ -55,17 +51,15 @@
     """
     if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_flag_url"):
         return get_comment_app().get_flag_url(comment)
-    else:
-        return urlresolvers.reverse("django.contrib.comments.views.moderation.flag", args=(comment.id,))
+    return urlresolvers.reverse("django.contrib.comments.views.moderation.flag", args=(comment.id,))
 
 def get_delete_url(comment):
     """
     Get the URL for the "delete this comment" view.
     """
     if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_delete_url"):
-        return get_comment_app().get_flag_url(get_delete_url)
-    else:
-        return urlresolvers.reverse("django.contrib.comments.views.moderation.delete", args=(comment.id,))
+        return get_comment_app().get_delete_url(comment)
+    return urlresolvers.reverse("django.contrib.comments.views.moderation.delete", args=(comment.id,))
 
 def get_approve_url(comment):
     """
@@ -73,5 +67,4 @@
     """
     if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_approve_url"):
         return get_comment_app().get_approve_url(comment)
-    else:
-        return urlresolvers.reverse("django.contrib.comments.views.moderation.approve", args=(comment.id,))
+    return urlresolvers.reverse("django.contrib.comments.views.moderation.approve", args=(comment.id,))
Index: tests/regressiontests/comment_tests/tests/app_api_tests.py
===================================================================
--- tests/regressiontests/comment_tests/tests/app_api_tests.py	(revision 9776)
+++ tests/regressiontests/comment_tests/tests/app_api_tests.py	(working copy)
@@ -28,3 +28,44 @@
         c = Comment(id=12345)
         self.assertEqual(comments.get_approve_url(c), "/approve/12345/")
 
+
+class CustomCommentTest(CommentTestCase):
+    urls = 'regressiontests.comment_tests.urls'
+
+    def setUp(self):
+        self.old_comments_app   = getattr(settings, 'COMMENTS_APP', None)
+        settings.COMMENTS_APP   = 'regressiontests.comment_tests.custom_comments'
+        settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + [settings.COMMENTS_APP,]
+
+    def tearDown(self):
+        del settings.INSTALLED_APPS[-1]
+        settings.COMMENTS_APP = self.old_comments_app
+        if settings.COMMENTS_APP is None:
+            delattr(settings._target, 'COMMENTS_APP')
+
+    def testGetCommentApp(self):
+        from regressiontests.comment_tests import custom_comments
+        self.assertEqual(comments.get_comment_app(), custom_comments)
+
+    def testGetModel(self):
+        from regressiontests.comment_tests.custom_comments.models import CustomComment
+        self.assertEqual(comments.get_model(), CustomComment)
+
+    def testGetForm(self):
+        from regressiontests.comment_tests.custom_comments.forms import CustomCommentForm
+        self.assertEqual(comments.get_form(), CustomCommentForm)
+
+    def testGetFormTarget(self):
+        self.assertEqual(comments.get_form_target(), "/post/")
+
+    def testGetFlagURL(self):
+        c = Comment(id=12345)
+        self.assertEqual(comments.get_flag_url(c), "/flag/12345/")
+
+    def getGetDeleteURL(self):
+        c = Comment(id=12345)
+        self.assertEqual(comments.get_delete_url(c), "/delete/12345/")
+
+    def getGetApproveURL(self):
+        c = Comment(id=12345)
+        self.assertEqual(comments.get_approve_url(c), "/approve/12345/")
Index: docs/ref/contrib/comments/index.txt
===================================================================
--- docs/ref/contrib/comments/index.txt	(revision 9776)
+++ docs/ref/contrib/comments/index.txt	(working copy)
@@ -164,6 +164,7 @@
 considerations you'll need to make if you're using this aproach.
 
 .. templatetag:: comment_form_target
+.. _notes-on-the-comment-form:
 
 Getting the comment form target
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -212,4 +213,4 @@
    settings
    signals
    upgrade
-
+   custom
Index: docs/ref/contrib/comments/settings.txt
===================================================================
--- docs/ref/contrib/comments/settings.txt	(revision 9776)
+++ docs/ref/contrib/comments/settings.txt	(working copy)
@@ -29,6 +29,7 @@
 COMMENTS_APP
 ------------
 
-The app (i.e. entry in ``INSTALLED_APPS``) responsible for all "business logic."
-You can change this to provide custom comment models and forms, though this is
-currently undocumented.
+An app which provides :ref:`customization of the comments framework
+<ref-contrib-comments-custom>`.  Use the same dotted-string notation
+as in :setting:`INSTALLED_APPS`.  Your custom :setting:`COMMENTS_APP`
+must also be listed in :setting:`INSTALLED_APPS`.
