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
This commit is contained in:
Jacob Kaplan-Moss 2009-02-23 22:16:26 +00:00
parent 7d4a954836
commit 63d85a684a
12 changed files with 333 additions and 29 deletions

View File

@ -77,6 +77,7 @@ answer newbie questions, and generally made Django that much better:
Trevor Caira <trevor@caira.com> Trevor Caira <trevor@caira.com>
Ricardo Javier Cárdenes Medina <ricardo.cardenes@gmail.com> Ricardo Javier Cárdenes Medina <ricardo.cardenes@gmail.com>
Jeremy Carbaugh <jcarbaugh@gmail.com> Jeremy Carbaugh <jcarbaugh@gmail.com>
carljm <carl@dirtcircle.com>
Graham Carlyle <graham.carlyle@maplecroft.net> Graham Carlyle <graham.carlyle@maplecroft.net>
Antonio Cavedoni <http://cavedoni.com/> Antonio Cavedoni <http://cavedoni.com/>
C8E C8E

View File

@ -1,9 +1,10 @@
from django.conf import settings from django.conf import settings
from django.core import urlresolvers from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured 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 DEFAULT_COMMENTS_APP = 'django.contrib.comments'
REQUIRED_COMMENTS_APP_ATTRIBUTES = ["get_model", "get_form", "get_form_target"]
def get_comment_app(): def get_comment_app():
""" """
@ -22,13 +23,6 @@ def get_comment_app():
raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\ raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\
"a non-existing package.") "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 return package
def get_comment_app_name(): 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 Returns the name of the comment app (either the setting value, if it
exists, or the default). exists, or the default).
""" """
return getattr(settings, 'COMMENTS_APP', 'django.contrib.comments') return getattr(settings, 'COMMENTS_APP', DEFAULT_COMMENTS_APP)
def get_model(): 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(): 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(): 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): def get_flag_url(comment):
""" """
Get the URL for the "flag this comment" view. 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) return get_comment_app().get_flag_url(comment)
else: 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): def get_delete_url(comment):
""" """
Get the URL for the "delete this comment" view. Get the URL for the "delete this comment" view.
""" """
if get_comment_app_name() != __name__ and hasattr(get_comment_app(), "get_delete_url"): if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_delete_url"):
return get_comment_app().get_flag_url(get_delete_url) return get_comment_app().get_delete_url(comment)
else: 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): def get_approve_url(comment):
""" """
Get the URL for the "approve this comment from moderation" view. 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) return get_comment_app().get_approve_url(comment)
else: 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 File

@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.comments.models import Comment from django.contrib.comments.models import Comment
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.comments import get_model
class CommentsAdmin(admin.ModelAdmin): class CommentsAdmin(admin.ModelAdmin):
fieldsets = ( fieldsets = (
@ -21,4 +22,7 @@ class CommentsAdmin(admin.ModelAdmin):
ordering = ('-submit_date',) ordering = ('-submit_date',)
search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') 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 File

@ -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 File

@ -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 #. Use the `comment template tags`_ below to embed comments in your
templates. 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 Comment template tags
===================== =====================
@ -161,7 +161,7 @@ A complete form might look like::
</form> </form>
Be sure to read the `notes on the comment form`_, below, for some special 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 .. templatetag:: comment_form_target
@ -175,6 +175,8 @@ you'll always want to use it like above::
<form action="{% comment_form_target %}" method="POST"> <form action="{% comment_form_target %}" method="POST">
.. _notes-on-the-comment-form:
Notes on the comment form Notes on the comment form
------------------------- -------------------------
@ -212,4 +214,4 @@ More information
settings settings
signals signals
upgrade upgrade
custom

View File

@ -29,6 +29,7 @@ this will be rejected. Defaults to 3000.
COMMENTS_APP COMMENTS_APP
------------ ------------
The app (i.e. entry in ``INSTALLED_APPS``) responsible for all "business logic." An app which provides :ref:`customization of the comments framework
You can change this to provide custom comment models and forms, though this is <ref-contrib-comments-custom>`. Use the same dotted-string notation
currently undocumented. as in :setting:`INSTALLED_APPS`. Your custom :setting:`COMMENTS_APP`
must also be listed in :setting:`INSTALLED_APPS`.

View File

@ -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 File

@ -0,0 +1,4 @@
from django import forms
class CustomCommentForm(forms.Form):
pass

View File

@ -0,0 +1,4 @@
from django.db import models
class CustomComment(models.Model):
pass

View File

@ -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 File

@ -28,3 +28,44 @@ class CommentAppAPITests(CommentTestCase):
c = Comment(id=12345) c = Comment(id=12345)
self.assertEqual(comments.get_approve_url(c), "/approve/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 File

@ -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'),
)