diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py
index 55b8bac..6a0287b 100644
--- a/django/contrib/comments/__init__.py
+++ b/django/contrib/comments/__init__.py
@@ -2,9 +2,6 @@ from django.conf import settings
 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 @@ def get_comment_app():
         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 @@ def get_comment_app_name():
     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 @@ def get_flag_url(comment):
     """
     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(get_delete_url)
+    return urlresolvers.reverse("django.contrib.comments.views.moderation.delete", args=(comment.id,))
 
 def get_approve_url(comment):
     """
@@ -73,5 +67,4 @@ def get_approve_url(comment):
     """
     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,))
diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py
index 11271c9..e2dfb67 100644
--- a/django/contrib/comments/admin.py
+++ b/django/contrib/comments/admin.py
@@ -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 @@ class CommentsAdmin(admin.ModelAdmin):
     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)
diff --git a/docs/ref/contrib/comments/custom.txt b/docs/ref/contrib/comments/custom.txt
new file mode 100644
index 0000000..7da3700
--- /dev/null
+++ b/docs/ref/contrib/comments/custom.txt
@@ -0,0 +1,211 @@
+.. _ref-contrib-comments-custom:
+
+==================================
+Customizing the comments framework
+==================================
+
+Via the :setting:`COMMENTS_APP` setting, the comments framework allows
+you to replace the built-in comment model and comment form with your
+own classes.
+
+The COMMENTS_APP
+================
+
+A custom :setting:`COMMENTS_APP` should define one or more of the
+following module-level functions in it's ``__init__.py`` file. None of
+these functions are required, so you can define any combination of
+them (all others will use the defaults from
+``django.contrib.comments``):
+
+.. function:: get_model()
+
+    Return the :class:`~django.db.models.Model` class to use for
+    comments.  This model should inherit from
+    :class:`django.contrib.comments.models.BaseCommentAbstractModel`,
+    which defines necessary core fields.
+
+    The default implementation returns
+    :class:`django.contrib.comments.models.Comment`.
+
+.. function:: get_form()
+
+    Return the :class:`~django.forms.Form` class you want to use for
+    creating, validating, and saving your comment model.  Your custom
+    comment form should accept an additional first argument,
+    ``target_object``, which is the object the comment will be
+    attached to.
+
+    The default implementation returns
+    :class:`django.contrib.comments.forms.CommentForm`.
+
+    .. note::
+
+        The default comment form also includes a number of unobtrusive
+        spam-prevention features (see
+        :ref:`notes-on-the-comment-form`).  If replacing it with your
+        own form, you may want to look at the source code for the
+        built-in form and consider incorporating similar features.
+
+.. function:: get_form_target()
+
+    Return the URL for POSTing comments.  This will be the ``action``
+    attribute when rendering your comment form.
+
+    The default implementation returns a reverse-resolved URL pointing
+    to the :func:`post_comment` view.
+
+    .. note::
+
+        If you provide a custom comment model and/or form, but you
+        want to use the default :func:`post_comment` view, you will
+        need to be aware that it requires the model and form to have
+        certain additional attributes and methods: see the
+        :func:`post_comment` view documentation for details.
+
+.. function:: get_flag_url()
+
+    Return the URL for the "flag this comment" view.
+
+    The default implementation returns a reverse-resolved URL pointing
+    to the :func:`django.contrib.comments.views.moderation.flag` view.
+
+.. function:: get_delete_url()
+
+    Return the URL for the "delete this comment" view.
+
+    The default implementation returns a reverse-resolved URL pointing
+    to the :func:`django.contrib.comments.views.moderation.delete` view.
+
+.. function:: get_approve_url()
+
+    Return the URL for the "approve this comment from moderation" view.
+
+    The default implementation returns a reverse-resolved URL pointing
+    to the :func:`django.contrib.comments.views.moderation.approve` view.
+
+A sample custom comments app
+----------------------------
+
+A custom comments app might have an ``__init__.py`` like this::
+
+    from django.core.urlresolvers import reverse
+
+    def get_model():
+        from my_comments_app.models import MyComment
+        return MyComment
+
+    def get_form():
+        from my_comments_app.forms import MyCommentForm
+        return MyCommentForm
+
+    def get_form_target():
+        return reverse('my_comments_app.views.post_comment')
+
+
+``MyComment`` should inherit from :class:`BaseCommentAbstractModel`,
+so in ``my_comments_app.models.py``::
+
+    from django.db import models
+    from django.contrib.comments.models import BaseCommentAbstractModel
+
+    class MyComment(BaseCommentAbstractModel):
+        ... fields and custom methods ...
+
+And ``MyCommentForm`` should accept a target_object argument,
+so in ``my_comments_app.forms.py``::
+
+    from django import forms
+
+    class MyCommentForm(forms.Form):
+        def __init__(self, target_object, data=None, initial=None):
+            ...
+
+In order to enable this custom comments app, you would need to have
+the following in your project's ``settings.py``::
+
+    INSTALLED_APPS = (
+    ...
+    'my_comments_app',
+    ...
+    )
+
+    COMMENTS_APP = 'my_comments_app'
+
+
+API reference
+==================================
+
+BaseCommentAbstractModel
+------------------------
+
+.. class:: BaseCommentAbstractModel
+
+    :class:`BaseCommentAbstractModel` defines the following attributes
+    and methods, which your custom comment model will inherit:
+
+    .. attribute:: site
+
+        A foreign key to the
+        :class:`~django.contrib.sites.models.Site` model (see
+        :ref:`ref-contrib-sites`), defining which site this comment
+        appears on.
+
+    .. attribute:: content_type
+
+        A foreign key to the
+        :class:`~django.contrib.contenttypes.models.ContentType`
+        model.  This is the content type of the target object the
+        comment is attached to.
+
+    .. attribute:: object_pk
+
+        The primary key of the target object.
+
+    .. attribute:: content_object
+
+        A
+        :class:`~django.contrib.contenttypes.generic.GenericForeignKey`
+        to the target object, using the :attr:`content_type` and
+        :attr:`object_pk` attributes.
+
+    .. method:: get_content_object_url()
+
+        Returns a URL that redirects to the URL of the comment's
+        target object.
+
+post_comment view
+-----------------
+
+.. function:: post_comment
+
+    The default :func:`post_comment` view requires the comment model
+    to have two additional fields:
+
+    .. attribute:: user
+
+        This should be a :class:`ForeignKey` to the :class:`User`
+        model.  The view will store the user who posted the comment in
+        this field.
+
+    .. attribute:: ip_address
+
+        This should be an :class:`IPAddressField`.  The
+        :func:`post_comment` view will store the remote IP address of
+        the comment poster in it.
+
+    The :func:`post_comment` view also requires the comment form to
+    have the following two methods:
+
+    .. method:: security_errors()
+
+        If this method returns a false value, the form will be
+        considered to have passed anti-spam screening.  If it returns
+        any nonzero value, that value will be coerced to a string and
+        sent as part of an HTTP 400 (bad request) response.
+
+    .. method:: get_comment_object()
+
+        This method will only be called if the form has passed
+        validation and returned no :func:`security_errors()`.  It
+        should return an (unsaved) comment object, which will be
+        annotated with the user and IP address and then saved.
diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt
index 5aeebe3..55140ce 100644
--- a/docs/ref/contrib/comments/index.txt
+++ b/docs/ref/contrib/comments/index.txt
@@ -164,6 +164,7 @@ Be sure to read the `notes on the comment form`_, below, for some special
 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 @@ More information
    settings
    signals
    upgrade
-
+   custom
diff --git a/docs/ref/contrib/comments/settings.txt b/docs/ref/contrib/comments/settings.txt
index 2aa392c..ff94d2d 100644
--- a/docs/ref/contrib/comments/settings.txt
+++ b/docs/ref/contrib/comments/settings.txt
@@ -29,6 +29,7 @@ this will be rejected. Defaults to 3000.
 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`.
diff --git a/tests/regressiontests/comment_tests/custom_comments/__init__.py b/tests/regressiontests/comment_tests/custom_comments/__init__.py
new file mode 100644
index 0000000..d644b53
--- /dev/null
+++ b/tests/regressiontests/comment_tests/custom_comments/__init__.py
@@ -0,0 +1,21 @@
+from django.core import urlresolvers
+
+def get_model():
+    from regressiontests.comment_tests.custom_comments.models import CustomComment 
+    return CustomComment 
+
+def get_form():
+    from regressiontests.comment_tests.custom_comments.forms import CustomCommentForm
+    return CustomCommentForm
+
+def get_form_target():
+    return urlresolvers.reverse("regressiontests.comment_tests.custom_comments.views.submit_comment")
+
+def get_flag_url(c):
+    return urlresolvers.reverse("regressiontests.comment_tests.custom_comments.views.flag_comment", args=(c.id,))
+
+def get_delete_url(c):
+    return urlresolvers.reverse("regressiontests.comment_tests.custom_comments.views.delete_comment", args=(c.id,))
+
+def get_approve_url(c):
+    return urlresolvers.reverse("regressiontests.comment_tests.custom_comments.views.approve_comment", args=(c.id,))
diff --git a/tests/regressiontests/comment_tests/custom_comments/forms.py b/tests/regressiontests/comment_tests/custom_comments/forms.py
new file mode 100644
index 0000000..b788cdc
--- /dev/null
+++ b/tests/regressiontests/comment_tests/custom_comments/forms.py
@@ -0,0 +1,4 @@
+from django import forms
+
+class CustomCommentForm(forms.Form):
+    pass
diff --git a/tests/regressiontests/comment_tests/custom_comments/models.py b/tests/regressiontests/comment_tests/custom_comments/models.py
new file mode 100644
index 0000000..592ad79
--- /dev/null
+++ b/tests/regressiontests/comment_tests/custom_comments/models.py
@@ -0,0 +1,4 @@
+from django.db import models
+
+class CustomComment(models.Model):
+    pass
diff --git a/tests/regressiontests/comment_tests/custom_comments/views.py b/tests/regressiontests/comment_tests/custom_comments/views.py
new file mode 100644
index 0000000..04a1d23
--- /dev/null
+++ b/tests/regressiontests/comment_tests/custom_comments/views.py
@@ -0,0 +1,11 @@
+def submit_comment(request):
+    return None
+
+def flag_comment(request, comment_id):
+    return None
+
+def delete_comment(request, comment_id):
+    return None
+
+def approve_comment(request, comment_id):
+    return None
diff --git a/tests/regressiontests/comment_tests/tests/app_api_tests.py b/tests/regressiontests/comment_tests/tests/app_api_tests.py
index d4a4488..19b9f1a 100644
--- a/tests/regressiontests/comment_tests/tests/app_api_tests.py
+++ b/tests/regressiontests/comment_tests/tests/app_api_tests.py
@@ -28,3 +28,44 @@ class CommentAppAPITests(CommentTestCase):
         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/")
diff --git a/tests/regressiontests/comment_tests/urls.py b/tests/regressiontests/comment_tests/urls.py
new file mode 100644
index 0000000..b72f677
--- /dev/null
+++ b/tests/regressiontests/comment_tests/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('regressiontests.comment_tests.custom_comments.views',
+    url(r'^post/$',          'submit_comment'),
+    url(r'^flag/(\d+)/$',    'flag_comment'),
+    url(r'^delete/(\d+)/$',  'delete_comment'),
+    url(r'^approve/(\d+)/$', 'approve_comment'),
+)
+
