Commit cba91997 authored by Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss
Browse files

Refactored Django's comment system.

Much of this work was done by Thejaswi Puthraya as part of Google's Summer of Code project; much thanks to him for the work, and to them for the program.

This is a backwards-incompatible change; see the upgrading guide in docs/ref/contrib/comments/upgrade.txt for instructions if you were using the old comments system.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8557 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent b46e736c
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -322,6 +322,7 @@ answer newbie questions, and generally made Django that much better:
    polpak@yahoo.com
    Matthias Pronk <django@masida.nl>
    Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
    Thejaswi Puthraya <thejaswi.puthraya@gmail.com>
    Johann Queuniet <johann.queuniet@adh.naellia.eu>
    Jan Rademaker
    Michael Radziej <mir@noris.de>
+70 −0
Original line number Diff line number Diff line
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
    """
    # Make sure the app's in INSTALLED_APPS
    comments_app = getattr(settings, 'COMMENTS_APP', 'django.contrib.comments')
    if comments_app not in settings.INSTALLED_APPS:
        raise ImproperlyConfigured("The COMMENTS_APP (%r) "\
                                   "must be in INSTALLED_APPS" % settings.COMMENTS_APP)

    # Try to import the package
    try:
        package = __import__(settings.COMMENTS_APP, '', '', [''])
    except ImportError:
        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_model():
    from django.contrib.comments.models import Comment
    return Comment

def get_form():
    from django.contrib.comments.forms import CommentForm
    return CommentForm

def get_form_target():
    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 settings.COMMENTS_APP != __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,))

def get_delete_url(comment):
    """
    Get the URL for the "delete this comment" view.
    """
    if settings.COMMENTS_APP != __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,))

def get_approve_url(comment):
    """
    Get the URL for the "approve this comment from moderation" view.
    """
    if settings.COMMENTS_APP != __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,))
+18 −24
Original line number Diff line number Diff line
from django.contrib import admin
from django.contrib.comments.models import Comment, FreeComment
from django.conf import settings
from django.contrib.comments.models import Comment
from django.utils.translation import ugettext_lazy as _


class CommentAdmin(admin.ModelAdmin):
class CommentsAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {'fields': ('content_type', 'object_id', 'site')}),
        ('Content', {'fields': ('user', 'headline', 'comment')}),
        ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}),
        ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}),
        (None,
           {'fields': ('content_type', 'object_pk', 'site')}
        ),
        (_('Content'),
           {'fields': ('user', 'user_name', 'user_email', 'user_url', 'comment')}
        ),
        (_('Metadata'),
           {'fields': ('submit_date', 'ip_address', 'is_public', 'is_removed')}
        ),
     )
    list_display = ('user', 'submit_date', 'content_type', 'get_content_object')
    list_filter = ('submit_date',)
    date_hierarchy = 'submit_date'
    search_fields = ('comment', 'user__username')
    raw_id_fields = ('user',)

class FreeCommentAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {'fields': ('content_type', 'object_id', 'site')}),
        ('Content', {'fields': ('person_name', 'comment')}),
        ('Meta', {'fields': ('is_public', 'ip_address', 'approved')}),
    )
    list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object')
    list_filter = ('submit_date',)
    list_display = ('name', 'content_type', 'object_pk', 'ip_address', 'is_public', 'is_removed')
    list_filter = ('submit_date', 'site', 'is_public', 'is_removed')
    date_hierarchy = 'submit_date'
    search_fields = ('comment', 'person_name')
    search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')

admin.site.register(Comment, CommentAdmin)
admin.site.register(FreeComment, FreeCommentAdmin)
 No newline at end of file
admin.site.register(Comment, CommentsAdmin)
+13 −20
Original line number Diff line number Diff line
from django.conf import settings
from django.contrib.comments.models import Comment, FreeComment
from django.contrib.syndication.feeds import Feed
from django.contrib.sites.models import Site
from django.contrib import comments

class LatestFreeCommentsFeed(Feed):
    """Feed of latest free comments on the current site."""

    comments_class = FreeComment
class LatestCommentFeed(Feed):
    """Feed of latest comments on the current site."""

    def title(self):
        if not hasattr(self, '_site'):
@@ -23,22 +21,17 @@ class LatestFreeCommentsFeed(Feed):
            self._site = Site.objects.get_current()
        return u"Latest comments on %s" % self._site.name

    def get_query_set(self):
        return self.comments_class.objects.filter(site__pk=settings.SITE_ID, is_public=True)

    def items(self):
        return self.get_query_set()[:40]

class LatestCommentsFeed(LatestFreeCommentsFeed):
    """Feed of latest comments on the current site."""

    comments_class = Comment

    def get_query_set(self):
        qs = super(LatestCommentsFeed, self).get_query_set()
        qs = qs.filter(is_removed=False)
        if settings.COMMENTS_BANNED_USERS_GROUP:
        qs = comments.get_model().objects.filter(
            site__pk = settings.SITE_ID,
            is_public = True,
            is_removed = False,
        )
        if getattr(settings, 'COMMENTS_BANNED_USERS_GROUP', None):
            where = ['user_id NOT IN (SELECT user_id FROM auth_users_group WHERE group_id = %s)']
            params = [settings.COMMENTS_BANNED_USERS_GROUP]
            qs = qs.extra(where=where, params=params)
        return qs
        return qs[:40]
        
    def item_pubdate(self, item):
        return item.submit_date
 No newline at end of file
+159 −0
Original line number Diff line number Diff line
import re
import time
import datetime
from sha import sha
from django import forms
from django.forms.util import ErrorDict
from django.conf import settings
from django.http import Http404
from django.contrib.contenttypes.models import ContentType
from models import Comment
from django.utils.text import get_text_list
from django.utils.translation import ngettext
from django.utils.translation import ugettext_lazy as _

COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000)

class CommentForm(forms.Form):
    name          = forms.CharField(label=_("Name"), max_length=50)
    email         = forms.EmailField(label=_("Email address"))
    url           = forms.URLField(label=_("URL"), required=False)
    comment       = forms.CharField(label=_('Comment'), widget=forms.Textarea,
                                    max_length=COMMENT_MAX_LENGTH)
    honeypot      = forms.CharField(required=False,
                                    label=_('If you enter anything in this field '\
                                            'your comment will be treated as spam'))
    content_type  = forms.CharField(widget=forms.HiddenInput)
    object_pk     = forms.CharField(widget=forms.HiddenInput)
    timestamp     = forms.IntegerField(widget=forms.HiddenInput)
    security_hash = forms.CharField(min_length=40, max_length=40, widget=forms.HiddenInput)

    def __init__(self, target_object, data=None, initial=None):
        self.target_object = target_object
        if initial is None:
            initial = {}
        initial.update(self.generate_security_data())
        super(CommentForm, self).__init__(data=data, initial=initial)

    def get_comment_object(self):
        """
        Return a new (unsaved) comment object based on the information in this
        form. Assumes that the form is already validated and will throw a
        ValueError if not.

        Does not set any of the fields that would come from a Request object
        (i.e. ``user`` or ``ip_address``).
        """
        if not self.is_valid():
            raise ValueError("get_comment_object may only be called on valid forms")

        new = Comment(
            content_type = ContentType.objects.get_for_model(self.target_object),
            object_pk    = str(self.target_object._get_pk_val()),
            user_name    = self.cleaned_data["name"],
            user_email   = self.cleaned_data["email"],
            user_url     = self.cleaned_data["url"],
            comment      = self.cleaned_data["comment"],
            submit_date  = datetime.datetime.now(),
            site_id      = settings.SITE_ID,
            is_public    = True,
            is_removed   = False,
        )

        # Check that this comment isn't duplicate. (Sometimes people post comments
        # twice by mistake.) If it is, fail silently by returning the old comment.
        possible_duplicates = Comment.objects.filter(
            content_type = new.content_type,
            object_pk = new.object_pk,
            user_name = new.user_name,
            user_email = new.user_email,
            user_url = new.user_url,
        )
        for old in possible_duplicates:
            if old.submit_date.date() == new.submit_date.date() and old.comment == new.comment:
                return old

        return new

    def security_errors(self):
        """Return just those errors associated with security"""
        errors = ErrorDict()
        for f in ["honeypot", "timestamp", "security_hash"]:
            if f in self.errors:
                errors[f] = self.errors[f]
        return errors

    def clean_honeypot(self):
        """Check that nothing's been entered into the honeypot."""
        value = self.cleaned_data["honeypot"]
        if value:
            raise forms.ValidationError(self.fields["honeypot"].label)
        return value

    def clean_security_hash(self):
        """Check the security hash."""
        security_hash_dict = {
            'content_type' : self.data.get("content_type", ""),
            'object_pk' : self.data.get("object_pk", ""),
            'timestamp' : self.data.get("timestamp", ""),
        }
        expected_hash = self.generate_security_hash(**security_hash_dict)
        actual_hash = self.cleaned_data["security_hash"]
        if expected_hash != actual_hash:
            raise forms.ValidationError("Security hash check failed.")
        return actual_hash

    def clean_timestamp(self):
        """Make sure the timestamp isn't too far (> 2 hours) in the past."""
        ts = self.cleaned_data["timestamp"]
        if time.time() - ts > (2 * 60 * 60):
            raise forms.ValidationError("Timestamp check failed")
        return ts

    def clean_comment(self):
        """
        If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't
        contain anything in PROFANITIES_LIST.
        """
        comment = self.cleaned_data["comment"]
        if settings.COMMENTS_ALLOW_PROFANITIES == False:
            # Logic adapted from django.core.validators; it's not clear if they
            # should be used in newforms or will be deprecated along with the
            # rest of oldforms
            bad_words = [w for w in settings.PROFANITIES_LIST if w in comment.lower()]
            if bad_words:
                plural = len(bad_words) > 1
                raise forms.ValidationError(ngettext(
                    "Watch your mouth! The word %s is not allowed here.",
                    "Watch your mouth! The words %s are not allowed here.", plural) % \
                    get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in bad_words], 'and'))
        return comment

    def generate_security_data(self):
        """Generate a dict of security data for "initial" data."""
        timestamp = int(time.time())
        security_dict =   {
            'content_type'  : str(self.target_object._meta),
            'object_pk'     : str(self.target_object._get_pk_val()),
            'timestamp'     : str(timestamp),
            'security_hash' : self.initial_security_hash(timestamp),
        }
        return security_dict

    def initial_security_hash(self, timestamp):
        """
        Generate the initial security hash from self.content_object
        and a (unix) timestamp.
        """

        initial_security_dict = {
            'content_type' : str(self.target_object._meta),
            'object_pk' : str(self.target_object._get_pk_val()),
            'timestamp' : str(timestamp),
          }
        return self.generate_security_hash(**initial_security_dict)

    def generate_security_hash(self, content_type, object_pk, timestamp):
        """Generate a (SHA1) security hash from the provided info."""
        info = (content_type, object_pk, timestamp, settings.SECRET_KEY)
        return sha("".join(info)).hexdigest()
Loading