Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #8630: finished the custom comment app API that was left out of…

… 1.0. This means it's now possible to override any of the models, forms, or views used by the comment app; see the new custom comment app docs for details and an example. Thanks to Thejaswi Puthraya for the original patch, and to carljm for docs and tests.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@9890 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 63d85a684ac713b74a8ea5fee3757139229e45a5 1 parent 7d4a954
@jacobian jacobian authored
View
1  AUTHORS
@@ -77,6 +77,7 @@ answer newbie questions, and generally made Django that much better:
Trevor Caira <trevor@caira.com>
Ricardo Javier Cárdenes Medina <ricardo.cardenes@gmail.com>
Jeremy Carbaugh <jcarbaugh@gmail.com>
+ carljm <carl@dirtcircle.com>
Graham Carlyle <graham.carlyle@maplecroft.net>
Antonio Cavedoni <http://cavedoni.com/>
C8E
View
57 django/contrib/comments/__init__.py
@@ -1,9 +1,10 @@
from django.conf import settings
from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured
+from django.contrib.comments.models import Comment
+from django.contrib.comments.forms import CommentForm
-# Attributes required in the top-level app for COMMENTS_APP
-REQUIRED_COMMENTS_APP_ATTRIBUTES = ["get_model", "get_form", "get_form_target"]
+DEFAULT_COMMENTS_APP = 'django.contrib.comments'
def get_comment_app():
"""
@@ -22,13 +23,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():
@@ -36,42 +30,61 @@ def get_comment_app_name():
Returns the name of the comment app (either the setting value, if it
exists, or the default).
"""
- return getattr(settings, 'COMMENTS_APP', 'django.contrib.comments')
+ return getattr(settings, 'COMMENTS_APP', DEFAULT_COMMENTS_APP)
def get_model():
- from django.contrib.comments.models import Comment
- return Comment
+ """
+ Returns the comment model class.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_model"):
+ return get_comment_app().get_model()
+ else:
+ return Comment
def get_form():
- from django.contrib.comments.forms import CommentForm
- return CommentForm
+ """
+ Returns the comment ModelForm class.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_form"):
+ return get_comment_app().get_form()
+ else:
+ return CommentForm
def get_form_target():
- return urlresolvers.reverse("django.contrib.comments.views.comments.post_comment")
+ """
+ Returns the target URL for the comment form submission view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_form_target"):
+ return get_comment_app().get_form_target()
+ else:
+ return urlresolvers.reverse("django.contrib.comments.views.comments.post_comment")
def get_flag_url(comment):
"""
Get the URL for the "flag this comment" view.
"""
- if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_flag_url"):
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP 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)
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_delete_url"):
+ return get_comment_app().get_delete_url(comment)
else:
- return urlresolvers.reverse("django.contrib.comments.views.moderation.delete", args=(comment.id,))
+ return urlresolvers.reverse("django.contrib.comments.views.moderation.delete",
+ args=(comment.id,))
def get_approve_url(comment):
"""
Get the URL for the "approve this comment from moderation" view.
"""
- if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_approve_url"):
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP 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,))
View
6 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,7 @@ 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)
+# Only register the default admin if the model is the built-in comment model
+# (this won't be true if there's a custom comment app).
+if get_model() is Comment:
+ admin.site.register(Comment, CommentsAdmin)
View
180 docs/ref/contrib/comments/custom.txt
@@ -0,0 +1,180 @@
+.. _ref-contrib-comments-custom:
+
+==================================
+Customizing the comments framework
+==================================
+
+.. currentmodule:: django.contrib.comments
+
+If the built-in comment framework doesn't quite fit your needs, you can extend
+the comment app's behavior to add custom data and logic. The comments framework
+lets you extend the built-in comment model, the built-in comment form, and the
+various comment views.
+
+The :setting:`COMMENTS_APP` setting is where this customization begins. Set
+:setting:`COMMENTS_APP` to the name of the app you'd like to use to provide
+custom behavior. You'll use the same syntax as you'd use for
+:setting:`INSTALLED_APPS`, and the app given must also be in the
+:setting:`INSTALLED_APPS` list.
+
+For example, if you wanted to use an app named ``my_comment_app``, your
+settings file would contain::
+
+ INSTALLED_APPS = [
+ ...
+ 'my_comment_app',
+ ...
+ ]
+
+ COMMENTS_APP = 'my_comment_app'
+
+The app named in :setting:`COMMENTS_APP` provides its custom behavior by
+defining some module-level functions in the app's ``__init__.py``. The
+:ref:`complete list of these functions <custom-comment-app-api>` can be found
+below, but first let's look at a quick example.
+
+An example custom comments app
+==============================
+
+One of the most common types of customization is modifying the set of fields
+provided on the built-in comment model. For example, some sites that allow
+comments want the commentator to provide a title for their comment; the built-in
+comment model has no field for that title.
+
+To make this kind of customization, we'll need to do three things:
+
+ #. Create a custom comment :class:`~django.db.models.Model` that adds on the
+ "title" field.
+
+ #. Create a custom comment :class:`~django.forms.Form` that also adds this
+ "title" field.
+
+ #. Inform Django of these objects by defining a few functions in a
+ custom :setting:`COMMENTS_APP`.
+
+So, carrying on the example above, we're dealing with a typical app structure in
+the ``my_custom_app`` directory::
+
+ my_custom_app/
+ __init__.py
+ models.py
+ forms.py
+
+In the ``models.py`` we'll define a ``CommentWithTitle`` model::
+
+ from django.db import models
+ from django.contrib.comments.models import BaseCommentAbstractModel
+
+ class CommentWithTitle(BaseCommentAbstractModel):
+ title = models.CharField(max_length=300)
+
+All custom comment models must subclass :class:`BaseCommentAbstractModel`.
+
+Next, we'll define a custom comment form in ``forms.py``. This is a little more
+tricky: we have to both create a form and override
+:meth:`CommentForm.get_comment_model` and
+:meth:`CommentForm.get_comment_create_data` to return deal with our custom title
+field::
+
+ from django import forms
+ from django.contrib.comments.forms import CommentForm
+ from my_comment_app.models import CommentWithTitle
+
+ class CommentFormWithTitle(CommentForm):
+ title = forms.CharField(max_length=300)
+
+ def get_comment_model(self):
+ # Use our custom comment model instead of the built-in one.
+ return CommentWithTitle
+
+ def get_comment_create_data(self):
+ # Use the data of the superclass, and add in the title field
+ data = super(CommentFormWithTitle, self).get_comment_create_data()
+ data['title'] = self.cleaned_data['title']
+ return data
+
+Finally, we'll define a couple of methods in ``my_custom_app/__init__.py`` to point Django at these classes we've created::
+
+ from my_comments_app.models import CommentWithTitle
+ from my_comments_app.forms import CommentFormWithTitle
+
+ def get_model():
+ return CommentWithTitle
+
+ def get_form():
+ return CommentFormWithTitle
+
+The above process should take care of most common situations. For more advanced usage, there are additional methods you can define. Those are explained in the next section.
+
+.. _custom-comment-app-api:
+
+Custom comment app API
+======================
+
+The :mod:`django.contrib.comments` app defines the following methods; any custom comment app must define at least one of them. All are optional, however.
+
+.. 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 ``<form 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.
View
8 docs/ref/contrib/comments/index.txt
@@ -42,7 +42,7 @@ To get started using the ``comments`` app, follow these steps:
#. Use the `comment template tags`_ below to embed comments in your
templates.
-You might also want to examine the :ref:`ref-contrib-comments-settings`
+You might also want to examine :ref:`ref-contrib-comments-settings`.
Comment template tags
=====================
@@ -161,7 +161,7 @@ A complete form might look like::
</form>
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.
+considerations you'll need to make if you're using this approach.
.. templatetag:: comment_form_target
@@ -175,6 +175,8 @@ you'll always want to use it like above::
<form action="{% comment_form_target %}" method="POST">
+.. _notes-on-the-comment-form:
+
Notes on the comment form
-------------------------
@@ -212,4 +214,4 @@ More information
settings
signals
upgrade
-
+ custom
View
7 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`.
View
32 tests/regressiontests/comment_tests/custom_comments/__init__.py
@@ -0,0 +1,32 @@
+from django.core import urlresolvers
+from regressiontests.comment_tests.custom_comments.models import CustomComment
+from regressiontests.comment_tests.custom_comments.forms import CustomCommentForm
+
+def get_model():
+ return CustomComment
+
+def get_form():
+ return CustomCommentForm
+
+def get_form_target():
+ return urlresolvers.reverse(
+ "regressiontests.comment_tests.custom_comments.views.custom_submit_comment"
+ )
+
+def get_flag_url(c):
+ return urlresolvers.reverse(
+ "regressiontests.comment_tests.custom_comments.views.custom_flag_comment",
+ args=(c.id,)
+ )
+
+def get_delete_url(c):
+ return urlresolvers.reverse(
+ "regressiontests.comment_tests.custom_comments.views.custom_delete_comment",
+ args=(c.id,)
+ )
+
+def get_approve_url(c):
+ return urlresolvers.reverse(
+ "regressiontests.comment_tests.custom_comments.views.custom_approve_comment",
+ args=(c.id,)
+ )
View
4 tests/regressiontests/comment_tests/custom_comments/forms.py
@@ -0,0 +1,4 @@
+from django import forms
+
+class CustomCommentForm(forms.Form):
+ pass
View
4 tests/regressiontests/comment_tests/custom_comments/models.py
@@ -0,0 +1,4 @@
+from django.db import models
+
+class CustomComment(models.Model):
+ pass
View
13 tests/regressiontests/comment_tests/custom_comments/views.py
@@ -0,0 +1,13 @@
+from django.http import HttpResponse
+
+def custom_submit_comment(request):
+ return HttpResponse("Hello from the custom submit comment view.")
+
+def custom_flag_comment(request, comment_id):
+ return HttpResponse("Hello from the custom flag view.")
+
+def custom_delete_comment(request, comment_id):
+ return HttpResponse("Hello from the custom delete view.")
+
+def custom_approve_comment(request, comment_id):
+ return HttpResponse("Hello from the custom approve view.")
View
41 tests/regressiontests/comment_tests/tests/app_api_tests.py
@@ -28,3 +28,44 @@ def getGetApproveURL(self):
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/")
View
9 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/$', 'custom_submit_comment'),
+ url(r'^flag/(\d+)/$', 'custom_flag_comment'),
+ url(r'^delete/(\d+)/$', 'custom_delete_comment'),
+ url(r'^approve/(\d+)/$', 'custom_approve_comment'),
+)
+
Please sign in to comment.
Something went wrong with that request. Please try again.