diff --git a/AUTHORS b/AUTHORS index e0f5bf2f7e..e77a30c0df 100644 --- a/AUTHORS +++ b/AUTHORS @@ -77,6 +77,7 @@ answer newbie questions, and generally made Django that much better: Trevor Caira Ricardo Javier Cárdenes Medina Jeremy Carbaugh + carljm Graham Carlyle Antonio Cavedoni C8E diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py index 55b8bac94a..b5c5acd254 100644 --- a/django/contrib/comments/__init__.py +++ b/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,)) diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index 11271c9d5b..3b1fb14bcc 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,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) diff --git a/docs/ref/contrib/comments/custom.txt b/docs/ref/contrib/comments/custom.txt new file mode 100644 index 0000000000..064bbca6d8 --- /dev/null +++ b/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 ` 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 ``
`` + 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. \ No newline at end of file diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt index 5aeebe32d7..b78ac4f36d 100644 --- a/docs/ref/contrib/comments/index.txt +++ b/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::
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::
+.. _notes-on-the-comment-form: + Notes on the comment form ------------------------- @@ -212,4 +214,4 @@ More information settings signals upgrade - + custom diff --git a/docs/ref/contrib/comments/settings.txt b/docs/ref/contrib/comments/settings.txt index 2aa392c260..ff94d2dbcc 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 +`. 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 0000000000..598927eace --- /dev/null +++ b/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,) + ) 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 0000000000..b788cdcf29 --- /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 0000000000..592ad79586 --- /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 0000000000..93cea9d591 --- /dev/null +++ b/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.") diff --git a/tests/regressiontests/comment_tests/tests/app_api_tests.py b/tests/regressiontests/comment_tests/tests/app_api_tests.py index d4a4488ab5..6a9eb1c637 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 0000000000..0058689657 --- /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/$', 'custom_submit_comment'), + url(r'^flag/(\d+)/$', 'custom_flag_comment'), + url(r'^delete/(\d+)/$', 'custom_delete_comment'), + url(r'^approve/(\d+)/$', 'custom_approve_comment'), +) +