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
This commit is contained in:
Jacob Kaplan-Moss 2008-08-25 22:14:22 +00:00
parent b46e736c9a
commit cba91997a2
49 changed files with 2410 additions and 1148 deletions

View File

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

View File

@ -0,0 +1,70 @@
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,))

View File

@ -1,30 +1,24 @@
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')}),
)
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',)
(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')}
),
)
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)
admin.site.register(Comment, CommentsAdmin)

View File

@ -1,12 +1,10 @@
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

View File

@ -0,0 +1,159 @@
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()

View File

@ -0,0 +1,22 @@
from django.db import models
from django.dispatch import dispatcher
from django.contrib.contenttypes.models import ContentType
class CommentManager(models.Manager):
def in_moderation(self):
"""
QuerySet for all comments currently in the moderation queue.
"""
return self.get_query_set().filter(is_public=False, is_removed=False)
def for_model(self, model):
"""
QuerySet for all comments for a particular model (either an instance or
a class).
"""
ct = ContentType.objects.get_for_model(model)
qs = self.get_query_set().filter(content_type=ct)
if isinstance(model, models.Model):
qs = qs.filter(object_pk=model._get_pk_val())
return qs

View File

@ -1,286 +1,185 @@
import datetime
from django.db import models
from django.contrib.auth.models import User
from django.contrib.comments.managers import CommentManager
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.contrib.auth.models import User
from django.db import models
from django.core import urlresolvers, validators
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
MIN_PHOTO_DIMENSION = 5
MAX_PHOTO_DIMENSION = 1000
COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000)
# Option codes for comment-form hidden fields.
PHOTOS_REQUIRED = 'pr'
PHOTOS_OPTIONAL = 'pa'
RATINGS_REQUIRED = 'rr'
RATINGS_OPTIONAL = 'ra'
IS_PUBLIC = 'ip'
class BaseCommentAbstractModel(models.Model):
"""
An abstract base class that any custom comment models probably should
subclass.
"""
# Content-object field
content_type = models.ForeignKey(ContentType)
object_pk = models.TextField(_('object ID'))
content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
# What users get if they don't have any karma.
DEFAULT_KARMA = 5
KARMA_NEEDED_BEFORE_DISPLAYED = 3
# Metadata about the comment
site = models.ForeignKey(Site)
class Meta:
abstract = True
class CommentManager(models.Manager):
def get_security_hash(self, options, photo_options, rating_options, target):
def get_content_object_url(self):
"""
Returns the MD5 hash of the given options (a comma-separated string such as
'pa,ra') and target (something like 'lcom.eventtimes:5157'). Used to
validate that submitted form options have not been tampered-with.
Get a URL suitable for redirecting to the content object. Uses the
``django.views.defaults.shortcut`` view, which thus must be installed.
"""
from django.utils.hashcompat import md5_constructor
return md5_constructor(options + photo_options + rating_options + target + settings.SECRET_KEY).hexdigest()
return urlresolvers.reverse(
"django.views.defaults.shortcut",
args=(self.content_type_id, self.object_pk)
)
def get_rating_options(self, rating_string):
"""
Given a rating_string, this returns a tuple of (rating_range, options).
>>> s = "scale:1-10|First_category|Second_category"
>>> Comment.objects.get_rating_options(s)
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category'])
"""
rating_range, options = rating_string.split('|', 1)
rating_range = range(int(rating_range[6:].split('-')[0]), int(rating_range[6:].split('-')[1])+1)
choices = [c.replace('_', ' ') for c in options.split('|')]
return rating_range, choices
class Comment(BaseCommentAbstractModel):
"""
A user comment about some object.
"""
def get_list_with_karma(self, **kwargs):
"""
Returns a list of Comment objects matching the given lookup terms, with
_karma_total_good and _karma_total_bad filled.
"""
extra_kwargs = {}
extra_kwargs.setdefault('select', {})
extra_kwargs['select']['_karma_total_good'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=1'
extra_kwargs['select']['_karma_total_bad'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=-1'
return self.filter(**kwargs).extra(**extra_kwargs)
# Who posted this comment? If ``user`` is set then it was an authenticated
# user; otherwise at least person_name should have been set and the comment
# was posted by a non-authenticated user.
user = models.ForeignKey(User, blank=True, null=True, related_name="%(class)s_comments")
user_name = models.CharField(_("user's name"), max_length=50, blank=True)
user_email = models.EmailField(_("user's email address"), blank=True)
user_url = models.URLField(_("user's URL"), blank=True)
def user_is_moderator(self, user):
if user.is_superuser:
return True
for g in user.groups.all():
if g.id == settings.COMMENTS_MODERATORS_GROUP:
return True
return False
comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
# Metadata about the comment
submit_date = models.DateTimeField(_('date/time submitted'), default=None)
ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
is_public = models.BooleanField(_('is public'), default=True,
help_text=_('Uncheck this box to make the comment effectively ' \
'disappear from the site.'))
is_removed = models.BooleanField(_('is removed'), default=False,
help_text=_('Check this box if the comment is inappropriate. ' \
'A "This comment has been removed" message will ' \
'be displayed instead.'))
class Comment(models.Model):
"""A comment by a registered user."""
user = models.ForeignKey(User)
content_type = models.ForeignKey(ContentType)
object_id = models.IntegerField(_('object ID'))
headline = models.CharField(_('headline'), max_length=255, blank=True)
comment = models.TextField(_('comment'), max_length=3000)
rating1 = models.PositiveSmallIntegerField(_('rating #1'), blank=True, null=True)
rating2 = models.PositiveSmallIntegerField(_('rating #2'), blank=True, null=True)
rating3 = models.PositiveSmallIntegerField(_('rating #3'), blank=True, null=True)
rating4 = models.PositiveSmallIntegerField(_('rating #4'), blank=True, null=True)
rating5 = models.PositiveSmallIntegerField(_('rating #5'), blank=True, null=True)
rating6 = models.PositiveSmallIntegerField(_('rating #6'), blank=True, null=True)
rating7 = models.PositiveSmallIntegerField(_('rating #7'), blank=True, null=True)
rating8 = models.PositiveSmallIntegerField(_('rating #8'), blank=True, null=True)
# This field designates whether to use this row's ratings in aggregate
# functions (summaries). We need this because people are allowed to post
# multiple reviews on the same thing, but the system will only use the
# latest one (with valid_rating=True) in tallying the reviews.
valid_rating = models.BooleanField(_('is valid rating'))
submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True)
is_public = models.BooleanField(_('is public'))
ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
is_removed = models.BooleanField(_('is removed'), help_text=_('Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.'))
site = models.ForeignKey(Site)
# Manager
objects = CommentManager()
class Meta:
verbose_name = _('comment')
verbose_name_plural = _('comments')
ordering = ('-submit_date',)
db_table = "django_comments"
ordering = ('submit_date',)
permissions = [("can_moderate", "Can moderate comments")]
def __unicode__(self):
return "%s: %s..." % (self.user.username, self.comment[:100])
return "%s: %s..." % (self.name, self.comment[:50])
def get_absolute_url(self):
try:
return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
except AttributeError:
return ""
def save(self):
if self.submit_date is None:
self.submit_date = datetime.datetime.now()
super(Comment, self).save()
def get_crossdomain_url(self):
return "/r/%d/%d/" % (self.content_type_id, self.object_id)
def get_flag_url(self):
return "/comments/flag/%s/" % self.id
def get_deletion_url(self):
return "/comments/delete/%s/" % self.id
def get_content_object(self):
def _get_userinfo(self):
"""
Returns the object that this comment is a comment on. Returns None if
the object no longer exists.
Get a dictionary that pulls together information about the poster
safely for both authenticated and non-authenticated comments.
This dict will have ``name``, ``email``, and ``url`` fields.
"""
from django.core.exceptions import ObjectDoesNotExist
try:
return self.content_type.get_object_for_this_type(pk=self.object_id)
except ObjectDoesNotExist:
return None
if not hasattr(self, "_userinfo"):
self._userinfo = {
"name" : self.user_name,
"email" : self.user_email,
"url" : self.user_url
}
if self.user_id:
u = self.user
if u.email:
self._userinfo["email"] = u.email
get_content_object.short_description = _('Content object')
# If the user has a full name, use that for the user name.
# However, a given user_name overrides the raw user.username,
# so only use that if this comment has no associated name.
if u.get_full_name():
self._userinfo["name"] = self.user.get_full_name()
elif not self.user_name:
self._userinfo["name"] = u.username
return self._userinfo
userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__)
def _fill_karma_cache(self):
"""Helper function that populates good/bad karma caches."""
good, bad = 0, 0
for k in self.karmascore_set:
if k.score == -1:
bad +=1
elif k.score == 1:
good +=1
self._karma_total_good, self._karma_total_bad = good, bad
def _get_name(self):
return self.userinfo["name"]
def _set_name(self, val):
if self.user_id:
raise AttributeError(_("This comment was posted by an authenticated "\
"user and thus the name is read-only."))
self.user_name = val
name = property(_get_name, _set_name, doc="The name of the user who posted this comment")
def get_good_karma_total(self):
if not hasattr(self, "_karma_total_good"):
self._fill_karma_cache()
return self._karma_total_good
def _get_email(self):
return self.userinfo["email"]
def _set_email(self, val):
if self.user_id:
raise AttributeError(_("This comment was posted by an authenticated "\
"user and thus the email is read-only."))
self.user_email = val
email = property(_get_email, _set_email, doc="The email of the user who posted this comment")
def get_bad_karma_total(self):
if not hasattr(self, "_karma_total_bad"):
self._fill_karma_cache()
return self._karma_total_bad
def _get_url(self):
return self.userinfo["url"]
def _set_url(self, val):
self.user_url = val
url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment")
def get_karma_total(self):
if not hasattr(self, "_karma_total_good") or not hasattr(self, "_karma_total_bad"):
self._fill_karma_cache()
return self._karma_total_good + self._karma_total_bad
def get_absolute_url(self, anchor_pattern="#c%(id)s"):
return self.get_content_object_url() + (anchor_pattern % self.__dict__)
def get_as_text(self):
return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % \
{'user': self.user.username, 'date': self.submit_date,
'comment': self.comment, 'domain': self.site.domain, 'url': self.get_absolute_url()}
"""
Return this comment as plain text. Useful for emails.
"""
d = {
'user': self.user,
'date': self.submit_date,
'comment': self.comment,
'domain': self.site.domain,
'url': self.get_absolute_url()
}
return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
class CommentFlag(models.Model):
"""
Records a flag on a comment. This is intentionally flexible; right now, a
flag could be:
class FreeComment(models.Model):
"""A comment by a non-registered user."""
content_type = models.ForeignKey(ContentType)
object_id = models.IntegerField(_('object ID'))
comment = models.TextField(_('comment'), max_length=3000)
person_name = models.CharField(_("person's name"), max_length=50)
submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True)
is_public = models.BooleanField(_('is public'))
ip_address = models.IPAddressField(_('ip address'))
# TODO: Change this to is_removed, like Comment
approved = models.BooleanField(_('approved by staff'))
site = models.ForeignKey(Site)
* A "removal suggestion" -- where a user suggests a comment for (potential) removal.
* A "moderator deletion" -- used when a moderator deletes a comment.
You can (ab)use this model to add other flags, if needed. However, by
design users are only allowed to flag a comment with a given flag once;
if you want rating look elsewhere.
"""
user = models.ForeignKey(User, related_name="comment_flags")
comment = models.ForeignKey(Comment, related_name="flags")
flag = models.CharField(max_length=30, db_index=True)
flag_date = models.DateTimeField(default=None)
# Constants for flag types
SUGGEST_REMOVAL = "removal suggestion"
MODERATOR_DELETION = "moderator deletion"
MODERATOR_APPROVAL = "moderator approval"
class Meta:
verbose_name = _('free comment')
verbose_name_plural = _('free comments')
ordering = ('-submit_date',)
db_table = 'django_comment_flags'
unique_together = [('user', 'comment', 'flag')]
def __unicode__(self):
return "%s: %s..." % (self.person_name, self.comment[:100])
return "%s flag of comment ID %s by %s" % \
(self.flag, self.comment_id, self.user.username)
def get_absolute_url(self):
try:
return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
except AttributeError:
return ""
def get_content_object(self):
"""
Returns the object that this comment is a comment on. Returns None if
the object no longer exists.
"""
from django.core.exceptions import ObjectDoesNotExist
try:
return self.content_type.get_object_for_this_type(pk=self.object_id)
except ObjectDoesNotExist:
return None
get_content_object.short_description = _('Content object')
class KarmaScoreManager(models.Manager):
def vote(self, user_id, comment_id, score):
try:
karma = self.get(comment__pk=comment_id, user__pk=user_id)
except self.model.DoesNotExist:
karma = self.model(None, user_id=user_id, comment_id=comment_id, score=score, scored_date=datetime.datetime.now())
karma.save()
else:
karma.score = score
karma.scored_date = datetime.datetime.now()
karma.save()
def get_pretty_score(self, score):
"""
Given a score between -1 and 1 (inclusive), returns the same score on a
scale between 1 and 10 (inclusive), as an integer.
"""
if score is None:
return DEFAULT_KARMA
return int(round((4.5 * score) + 5.5))
class KarmaScore(models.Model):
user = models.ForeignKey(User)
comment = models.ForeignKey(Comment)
score = models.SmallIntegerField(_('score'), db_index=True)
scored_date = models.DateTimeField(_('score date'), auto_now=True)
objects = KarmaScoreManager()
class Meta:
verbose_name = _('karma score')
verbose_name_plural = _('karma scores')
unique_together = (('user', 'comment'),)
def __unicode__(self):
return _("%(score)d rating by %(user)s") % {'score': self.score, 'user': self.user}
class UserFlagManager(models.Manager):
def flag(self, comment, user):
"""
Flags the given comment by the given user. If the comment has already
been flagged by the user, or it was a comment posted by the user,
nothing happens.
"""
if int(comment.user_id) == int(user.id):
return # A user can't flag his own comment. Fail silently.
try:
f = self.get(user__pk=user.id, comment__pk=comment.id)
except self.model.DoesNotExist:
from django.core.mail import mail_managers
f = self.model(None, user.id, comment.id, None)
message = _('This comment was flagged by %(user)s:\n\n%(text)s') % {'user': user.username, 'text': comment.get_as_text()}
mail_managers('Comment flagged', message, fail_silently=True)
f.save()
class UserFlag(models.Model):
user = models.ForeignKey(User)
comment = models.ForeignKey(Comment)
flag_date = models.DateTimeField(_('flag date'), auto_now_add=True)
objects = UserFlagManager()
class Meta:
verbose_name = _('user flag')
verbose_name_plural = _('user flags')
unique_together = (('user', 'comment'),)
def __unicode__(self):
return _("Flag by %r") % self.user
class ModeratorDeletion(models.Model):
user = models.ForeignKey(User, verbose_name='moderator')
comment = models.ForeignKey(Comment)
deletion_date = models.DateTimeField(_('deletion date'), auto_now_add=True)
class Meta:
verbose_name = _('moderator deletion')
verbose_name_plural = _('moderator deletions')
unique_together = (('user', 'comment'),)
def __unicode__(self):
return _("Moderator deletion by %r") % self.user
def save(self):
if self.flag_date is None:
self.flag_date = datetime.datetime.now()
super(CommentFlag, self).save()

View File

@ -0,0 +1,21 @@
"""
Signals relating to comments.
"""
from django.dispatch import Signal
# Sent just before a comment will be posted (after it's been approved and
# moderated; this can be used to modify the comment (in place) with posting
# details or other such actions. If any receiver returns False the comment will be
# discarded and a 403 (not allowed) response. This signal is sent at more or less
# the same time (just before, actually) as the Comment object's pre-save signal,
# except that the HTTP request is sent along with this signal.
comment_will_be_posted = Signal()
# Sent just after a comment was posted. See above for how this differs
# from the Comment object's post-save signal.
comment_was_posted = Signal()
# Sent after a comment was "flagged" in some way. Check the flag to see if this
# was a user requesting removal of a comment, a moderator approving/removing a
# comment, or some other custom user flag.
comment_was_flagged = Signal()

View File

@ -0,0 +1,53 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Comment post not allowed (400)</title>
<meta name="robots" content="NONE,NOARCHIVE" />
<style type="text/css">
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
body { font:small sans-serif; background:#eee; }
body>div { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; margin-bottom:.4em; }
h1 span { font-size:60%; color:#666; font-weight:normal; }
table { border:none; border-collapse: collapse; width:100%; }
td, th { vertical-align:top; padding:2px 3px; }
th { width:12em; text-align:right; color:#666; padding-right:.5em; }
#info { background:#f6f6f6; }
#info ol { margin: 0.5em 4em; }
#info ol li { font-family: monospace; }
#summary { background: #ffc; }
#explanation { background:#eee; border-bottom: 0px none; }
</style>
</head>
<body>
<div id="summary">
<h1>Comment post not allowed <span>(400)</span></h1>
<table class="meta">
<tr>
<th>Why:</th>
<td>{{ why }}</td>
</tr>
</table>
</div>
<div id="info">
<p>
The comment you tried to post to this view wasn't saved because something
tampered with the security information in the comment form. The message
above should explain the problem, or you can check the <a
href="http://www.djangoproject.com/documentation/comments/">comment
documentation</a> for more help.
</p>
</div>
<div id="explanation">
<p>
You're seeing this error because you have <code>DEBUG = True</code> in
your Django settings file. Change that to <code>False</code>, and Django
will display a standard 400 error page.
</p>
</div>
</body>
</html>

View File

@ -0,0 +1,14 @@
{% extends "comments/base.html" %}
{% block title %}Approve a comment{% endblock %}
{% block content %}
<h1>Really make this comment public?</h1>
<blockquote>{{ comment|escape|linebreaks }}</blockquote>
<form action="." method="POST">
<input type="hidden" name="next" value="{{ next|escape }}" id="next">
<p class="submit">
<input type="submit" name="submit" value="Approve"> or <a href="{{ comment.permalink }}">cancel</a>
</p>
</form>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "comments/base.html" %}
{% block title %}Thanks for approving.{% endblock %}
{% block content %}
<h1>Thanks for taking the time to improve the quality of discussion on our site.</h1>
{% endblock %}

View File

@ -0,0 +1,10 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,14 @@
{% extends "comments/base.html" %}
{% block title %}Remove a comment{% endblock %}
{% block content %}
<h1>Really remove this comment?</h1>
<blockquote>{{ comment|escape|linebreaks }}</blockquote>
<form action="." method="POST">
<input type="hidden" name="next" value="{{ next|escape }}" id="next">
<p class="submit">
<input type="submit" name="submit" value="Remove"> or <a href="{{ comment.permalink }}">cancel</a>
</p>
</form>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "comments/base.html" %}
{% block title %}Thanks for removing.{% endblock %}
{% block content %}
<h1>Thanks for taking the time to improve the quality of discussion on our site.</h1>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "comments/base.html" %}
{% block title %}Flag this comment{% endblock %}
{% block content %}
<h1>Really flag this comment?</h1>
<blockquote>{{ comment|escape|linebreaks }}</blockquote>
<form action="." method="POST">
<input type="hidden" name="next" value="{{ next|escape }}" id="next">
<p class="submit">
<input type="submit" name="submit" value="Flag"> or <a href="{{ comment.permalink }}">cancel</a>
</p>
</form>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "comments/base.html" %}
{% block title %}Thanks for flagging.{% endblock %}
{% block content %}
<h1>Thanks for taking the time to improve the quality of discussion on our site.</h1>
{% endblock %}

View File

@ -1,38 +1,19 @@
{% load i18n %}
{% if display_form %}
<form {% if photos_optional or photos_required %}enctype="multipart/form-data" {% endif %}action="/comments/post/" method="post">
{% if user.is_authenticated %}
<p>{% trans "Username:" %} <strong>{{ user.username }}</strong> (<a href="{{ logout_url }}">{% trans "Log out" %}</a>)</p>
{% else %}
<p><label for="id_username">{% trans "Username:" %}</label> <input type="text" name="username" id="id_username" /><br />{% trans "Password:" %} <input type="password" name="password" id="id_password" /> (<a href="/accounts/password_reset/">{% trans "Forgotten your password?" %}</a>)</p>
{% endif %}
{% if ratings_optional or ratings_required %}
<p>{% trans "Ratings" %} ({% if ratings_required %}{% trans "Required" %}{% else %}{% trans "Optional" %}{% endif %}):</p>
<table>
<tr><th>&nbsp;</th>{% for value in rating_range %}<th>{{ value }}</th>{% endfor %}</tr>
{% for rating in rating_choices %}
<tr><th>{{ rating }}</th>{% for value in rating_range %}<th><input type="radio" name="rating{{ forloop.parentloop.counter }}" value="{{ value }}" /></th>{% endfor %}</tr>
{% endfor %}
</table>
<input type="hidden" name="rating_options" value="{{ rating_options }}" />
{% endif %}
{% if photos_optional or photos_required %}
<p><label for="id_photo">{% trans "Post a photo" %}</label> ({% if photos_required %}{% trans "Required" %}{% else %}{% trans "Optional" %}{% endif %}):
<input type="file" name="photo" id="id_photo" /></p>
<input type="hidden" name="photo_options" value="{{ photo_options }}" />
{% endif %}
<p><label for="id_comment">{% trans "Comment:" %}</label><br />
<textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
<p>
<input type="hidden" name="options" value="{{ options }}" />
<input type="hidden" name="target" value="{{ target }}" />
<input type="hidden" name="gonzo" value="{{ hash }}" />
<input type="submit" name="preview" value="{% trans "Preview comment" %}" />
</p>
</form>
{% endif %}
{% load comments %}
<form action="{% comment_form_target %}" method="POST">
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<p
{% if field.errors %} class="error"{% endif %}
{% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
{% if field.errors %}{{ field.errors }}{% endif %}
{{ field.label_tag }} {{ field }}
</p>
{% endif %}
{% endfor %}
<p class="submit">
<input type="submit" name="submit" class="submit-post" value="Post">
<input type="submit" name="submit" class="submit-preview" value="Preview">
</p>
</form>

View File

@ -1,13 +0,0 @@
{% load i18n %}
{% if display_form %}
<form action="/comments/postfree/" method="post">
<p><label for="id_person_name">{% trans "Your name:" %}</label> <input type="text" id="id_person_name" name="person_name" /></p>
<p><label for="id_comment">{% trans "Comment:" %}</label><br /><textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
<p>
<input type="hidden" name="options" value="{{ options }}" />
<input type="hidden" name="target" value="{{ target }}" />
<input type="hidden" name="gonzo" value="{{ hash }}" />
<input type="submit" name="preview" value="{% trans "Preview comment" %}" />
</p>
</form>
{% endif %}

View File

@ -0,0 +1,75 @@
{% extends "admin/change_list.html" %}
{% load adminmedia %}
{% block title %}Comment moderation queue{% endblock %}
{% block extrahead %}
{{ block.super }}
<style type="text/css" media="screen">
p#nocomments { font-size: 200%; text-align: center; border: 1px #ccc dashed; padding: 4em; }
td.actions { width: 11em; }
td.actions form { display: inline; }
td.actions form input.submit { width: 5em; padding: 2px 4px; margin-right: 4px;}
td.actions form input.approve { background: green; color: white; }
td.actions form input.remove { background: red; color: white; }
</style>
{% endblock %}
{% block branding %}
<h1 id="site-name">Comment moderation queue</h1>
{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block content %}
{% if empty %}
<p id="nocomments">No comments to moderate.</div>
{% else %}
<div id="content-main">
<div class="module" id="changelist">
<table cellspacing="0">
<thead>
<tr>
<th>Action</th>
<th>Name</th>
<th>Comment</th>
<th>Email</th>
<th>URL</th>
<th>Authenticated?</th>
<th>IP Address</th>
<th class="sorted desc">Date posted</th>
</tr>
</thead>
<tbody>
{% for comment in comments %}
<tr class="{% cycle 'row1' 'row2' %}">
<td class="actions">
<form action="{% url comments-approve comment.pk %}" method="POST">
<input type="hidden" name="next" value="{% url comments-moderation-queue %}">
<input class="approve submit" type="submit" name="submit" value="Approve">
</form>
<form action="{% url comments-delete comment.pk %}" method="POST">
<input type="hidden" name="next" value="{% url comments-moderation-queue %}">
<input class="remove submit" type="submit" name="submit" value="Remove">
</form>
</td>
<td>{{ comment.name|escape }}</td>
<td>{{ comment.comment|truncatewords:"50"|escape }}</td>
<td>{{ comment.email|escape }}</td>
<td>{{ comment.url|escape }}</td>
<td>
<img
src="{% admin_media_prefix %}img/admin/icon-{% if comment.user %}yes{% else %}no{% endif %}.gif"
alt="{% if comment.user %}yes{% else %}no{% endif %}"
/>
</td>
<td>{{ comment.ip_address|escape }}</td>
<td>{{ comment.submit_date|date:"F j, P" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "comments/base.html" %}
{% block title %}Thanks for commenting.{% endblock %}
{% block content %}
<h1>Thank you for your comment.</h1>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends "comments/base.html" %}
{% block title %}Preview your comment{% endblock %}
{% block content %}
{% load comments %}
<form action="{% comment_form_target %}" method="POST">
{% if form.errors %}
<h1>Please correct the error{{ form.errors|pluralize }} below</h1>
{% else %}
<h1>Preview your comment</h1>
<blockquote>{{ comment|escape|linebreaks }}</blockquote>
<p>
and <input type="submit" name="submit" value="Post your comment" id="submit"> or make changes:
</p>
{% endif %}
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<p
{% if field.errors %} class="error"{% endif %}
{% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
{% if field.errors %}{{ field.errors }}{% endif %}
{{ field.label_tag }} {{ field }}
</p>
{% endif %}
{% endfor %}
<p class="submit">
<input type="submit" name="submit" class="submit-post" value="Post">
<input type="submit" name="submit" class="submit-preview" value="Preview">
</p>
</form>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% load comments %}
<form action="{% comment_form_target %}" method="POST">
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<p
{% if field.errors %} class="error"{% endif %}
{% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
{% if field.errors %}{{ field.errors }}{% endif %}
{{ field.label_tag }} {{ field }}
</p>
{% endif %}
{% endfor %}
<p class="submit">
<input type="submit" name="submit" class="submit-post" value="Reply">
<input type="submit" name="submit" class="submit-preview" value="Preview">
</p>
</form>

View File

@ -0,0 +1,34 @@
{% extends "comments/base.html" %}
{% block title %}Preview your comment{% endblock %}
{% block content %}
{% load comments %}
<form action="{% comment_form_target %}" method="POST">
{% if form.errors %}
<h1>Please correct the error{{ form.errors|pluralize }} below</h1>
{% else %}
<h1>Preview your comment</h1>
<blockquote>{{ comment|escape|linebreaks }}</blockquote>
<p>
and <input type="submit" name="submit" value="Post your comment" id="submit"> or make changes:
</p>
{% endif %}
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<p
{% if field.errors %} class="error"{% endif %}
{% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
{% if field.errors %}{{ field.errors }}{% endif %}
{{ field.label_tag }} {{ field }}
</p>
{% endif %}
{% endfor %}
<p class="submit">
<input type="submit" name="submit" class="submit-post" value="Post">
<input type="submit" name="submit" class="submit-preview" value="Preview">
</p>
</form>
{% endblock %}

View File

@ -1,332 +1,251 @@
from django.contrib.comments.models import Comment, FreeComment
from django.contrib.comments.models import PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
from django.contrib.comments.models import MIN_PHOTO_DIMENSION, MAX_PHOTO_DIMENSION
from django import template
from django.template import loader
from django.core.exceptions import ObjectDoesNotExist
from django.template.loader import render_to_string
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import smart_str
import re
from django.contrib import comments
register = template.Library()
COMMENT_FORM = 'comments/form.html'
FREE_COMMENT_FORM = 'comments/freeform.html'
class BaseCommentNode(template.Node):
"""
Base helper class (abstract) for handling the get_comment_* template tags.
Looks a bit strange, but the subclasses below should make this a bit more
obvious.
"""
class CommentFormNode(template.Node):
def __init__(self, content_type, obj_id_lookup_var, obj_id, free,
photos_optional=False, photos_required=False, photo_options='',
ratings_optional=False, ratings_required=False, rating_options='',
is_public=True):
self.content_type = content_type
if obj_id_lookup_var is not None:
obj_id_lookup_var = template.Variable(obj_id_lookup_var)
self.obj_id_lookup_var, self.obj_id, self.free = obj_id_lookup_var, obj_id, free
self.photos_optional, self.photos_required = photos_optional, photos_required
self.ratings_optional, self.ratings_required = ratings_optional, ratings_required
self.photo_options, self.rating_options = photo_options, rating_options
self.is_public = is_public
#@classmethod
def handle_token(cls, parser, token):
"""Class method to parse get_comment_list/count/form and return a Node."""
tokens = token.contents.split()
if tokens[1] != 'for':
raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0])
# {% get_whatever for obj as varname %}
if len(tokens) == 5:
if tokens[3] != 'as':
raise template.TemplateSyntaxError("Third argument in %r must be 'as'" % tokens[0])
return cls(
object_expr = parser.compile_filter(tokens[2]),
as_varname = tokens[4],
)
# {% get_whatever for app.model pk as varname %}
elif len(tokens) == 6:
if tokens[4] != 'as':
raise template.TemplateSyntaxError("Fourth argument in %r must be 'as'" % tokens[0])
return cls(
ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]),
object_pk_expr = parser.compile_filter(tokens[3]),
as_varname = tokens[5]
)
else:
raise template.TemplateSyntaxError("%r tag requires 4 or 5 arguments" % tokens[0])
handle_token = classmethod(handle_token)
#@staticmethod
def lookup_content_type(token, tagname):
try:
app, model = token.split('.')
return ContentType.objects.get(app_label=app, model=model)
except ValueError:
raise template.TemplateSyntaxError("Third argument in %r must be in the format 'app.model'" % tagname)
except ContentType.DoesNotExist:
raise template.TemplateSyntaxError("%r tag has non-existant content-type: '%s.%s'" % (tagname, app, model))
lookup_content_type = staticmethod(lookup_content_type)
def __init__(self, ctype=None, object_pk_expr=None, object_expr=None, as_varname=None, comment=None):
if ctype is None and object_expr is None:
raise template.TemplateSyntaxError("Comment nodes must be given either a literal object or a ctype and object pk.")
self.comment_model = comments.get_model()
self.as_varname = as_varname
self.ctype = ctype
self.object_pk_expr = object_pk_expr
self.object_expr = object_expr
self.comment = comment
def render(self, context):
from django.conf import settings
from django.utils.text import normalize_newlines
import base64
context.push()
if self.obj_id_lookup_var is not None:
try:
self.obj_id = self.obj_id_lookup_var.resolve(context)
except template.VariableDoesNotExist:
return ''
# Validate that this object ID is valid for this content-type.
# We only have to do this validation if obj_id_lookup_var is provided,
# because do_comment_form() validates hard-coded object IDs.
try:
self.content_type.get_object_for_this_type(pk=self.obj_id)
except ObjectDoesNotExist:
context['display_form'] = False
else:
context['display_form'] = True
else:
context['display_form'] = True
context['target'] = '%s:%s' % (self.content_type.id, self.obj_id)
options = []
for var, abbr in (('photos_required', PHOTOS_REQUIRED),
('photos_optional', PHOTOS_OPTIONAL),
('ratings_required', RATINGS_REQUIRED),
('ratings_optional', RATINGS_OPTIONAL),
('is_public', IS_PUBLIC)):
context[var] = getattr(self, var)
if getattr(self, var):
options.append(abbr)
context['options'] = ','.join(options)
if self.free:
context['hash'] = Comment.objects.get_security_hash(context['options'], '', '', context['target'])
default_form = loader.get_template(FREE_COMMENT_FORM)
else:
context['photo_options'] = self.photo_options
context['rating_options'] = normalize_newlines(base64.encodestring(self.rating_options).strip())
if self.rating_options:
context['rating_range'], context['rating_choices'] = Comment.objects.get_rating_options(self.rating_options)
context['hash'] = Comment.objects.get_security_hash(context['options'], context['photo_options'], context['rating_options'], context['target'])
context['logout_url'] = settings.LOGOUT_URL
default_form = loader.get_template(COMMENT_FORM)
output = default_form.render(context)
context.pop()
return output
class CommentCountNode(template.Node):
def __init__(self, package, module, context_var_name, obj_id, var_name, free):
self.package, self.module = package, module
if context_var_name is not None:
context_var_name = template.Variable(context_var_name)
self.context_var_name, self.obj_id = context_var_name, obj_id
self.var_name, self.free = var_name, free
def render(self, context):
from django.conf import settings
manager = self.free and FreeComment.objects or Comment.objects
if self.context_var_name is not None:
self.obj_id = self.context_var_name.resolve(context)
comment_count = manager.filter(object_id__exact=self.obj_id,
content_type__app_label__exact=self.package,
content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count()
context[self.var_name] = comment_count
qs = self.get_query_set(context)
context[self.as_varname] = self.get_context_value_from_queryset(context, qs)
return ''
class CommentListNode(template.Node):
def __init__(self, package, module, context_var_name, obj_id, var_name, free, ordering, extra_kwargs=None):
self.package, self.module = package, module
if context_var_name is not None:
context_var_name = template.Variable(context_var_name)
self.context_var_name, self.obj_id = context_var_name, obj_id
self.var_name, self.free = var_name, free
self.ordering = ordering
self.extra_kwargs = extra_kwargs or {}
def get_query_set(self, context):
ctype, object_pk = self.get_target_ctype_pk(context)
if not object_pk:
return self.comment_model.objects.none()
qs = self.comment_model.objects.filter(
content_type = ctype,
object_pk = object_pk,
site__pk = settings.SITE_ID,
is_public = True,
)
if settings.COMMENTS_HIDE_REMOVED:
qs = qs.filter(is_removed=False)
return qs
def get_target_ctype_pk(self, context):
if self.object_expr:
try:
obj = self.object_expr.resolve(context)
except template.VariableDoesNotExist:
return None, None
return ContentType.objects.get_for_model(obj), obj.pk
else:
return self.ctype, self.object_pk_expr.resolve(context, ignore_failures=True)
def get_context_value_from_queryset(self, context, qs):
"""Subclasses should override this."""
raise NotImplementedError
class CommentListNode(BaseCommentNode):
"""Insert a list of comments into the context."""
def get_context_value_from_queryset(self, context, qs):
return list(qs)
class CommentCountNode(BaseCommentNode):
"""Insert a count of comments into the context."""
def get_context_value_from_queryset(self, context, qs):
return qs.count()
class CommentFormNode(BaseCommentNode):
"""Insert a form for the comment model into the context."""
def get_form(self, context):
ctype, object_pk = self.get_target_ctype_pk(context)
if object_pk:
return comments.get_form()(ctype.get_object_for_this_type(pk=object_pk))
else:
return None
def render(self, context):
from django.conf import settings
get_list_function = self.free and FreeComment.objects.filter or Comment.objects.get_list_with_karma
if self.context_var_name is not None:
try:
self.obj_id = self.context_var_name.resolve(context)
except template.VariableDoesNotExist:
return ''
kwargs = {
'object_id__exact': self.obj_id,
'content_type__app_label__exact': self.package,
'content_type__model__exact': self.module,
'site__id__exact': settings.SITE_ID,
}
kwargs.update(self.extra_kwargs)
comment_list = get_list_function(**kwargs).order_by(self.ordering + 'submit_date').select_related()
if not self.free and settings.COMMENTS_BANNED_USERS_GROUP:
comment_list = comment_list.extra(select={'is_hidden': 'user_id IN (SELECT user_id FROM auth_user_groups WHERE group_id = %s)' % settings.COMMENTS_BANNED_USERS_GROUP})
if not self.free:
if 'user' in context and context['user'].is_authenticated():
user_id = context['user'].id
context['user_can_moderate_comments'] = Comment.objects.user_is_moderator(context['user'])
else:
user_id = None
context['user_can_moderate_comments'] = False
# Only display comments by banned users to those users themselves.
if settings.COMMENTS_BANNED_USERS_GROUP:
comment_list = [c for c in comment_list if not c.is_hidden or (user_id == c.user_id)]
context[self.var_name] = comment_list
context[self.as_varname] = self.get_form(context)
return ''
class DoCommentForm:
class RenderCommentFormNode(CommentFormNode):
"""Render the comment form directly"""
#@classmethod
def handle_token(cls, parser, token):
"""Class method to parse render_comment_form and return a Node."""
tokens = token.contents.split()
if tokens[1] != 'for':
raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0])
# {% render_comment_form for obj %}
if len(tokens) == 3:
return cls(object_expr=parser.compile_filter(tokens[2]))
# {% render_comment_form for app.models pk %}
elif len(tokens) == 4:
return cls(
ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]),
object_pk_expr = parser.compile_filter(tokens[3])
)
handle_token = classmethod(handle_token)
def render(self, context):
ctype, object_pk = self.get_target_ctype_pk(context)
if object_pk:
template_search_list = [
"comments/%s/%s/form.html" % (ctype.app_label, ctype.model),
"comments/%s/form.html" % ctype.app_label,
"comments/form.html"
]
context.push()
formstr = render_to_string(template_search_list, {"form" : self.get_form(context)}, context)
context.pop()
return formstr
else:
return ''
# We could just register each classmethod directly, but then we'd lose out on
# the automagic docstrings-into-admin-docs tricks. So each node gets a cute
# wrapper function that just exists to hold the docstring.
#@register.tag
def get_comment_count(parser, token):
"""
Displays a comment form for the given params.
Gets the comment count for the given params and populates the template
context with a variable containing that value, whose name is defined by the
'as' clause.
Syntax::
{% comment_form for [pkg].[py_module_name] [context_var_containing_obj_id] with [list of options] %}
{% get_comment_count for [object] as [varname] %}
{% get_comment_count for [app].[model] [object_id] as [varname] %}
Example usage::
{% comment_form for lcom.eventtimes event.id with is_public yes photos_optional thumbs,200,400 ratings_optional scale:1-5|first_option|second_option %}
{% get_comment_count for event as comment_count %}
{% get_comment_count for calendar.event event.id as comment_count %}
{% get_comment_count for calendar.event 17 as comment_count %}
``[context_var_containing_obj_id]`` can be a hard-coded integer or a variable containing the ID.
"""
def __init__(self, free):
self.free = free
return CommentCountNode.handle_token(parser, token)
def __call__(self, parser, token):
tokens = token.contents.split()
if len(tokens) < 4:
raise template.TemplateSyntaxError, "%r tag requires at least 3 arguments" % tokens[0]
if tokens[1] != 'for':
raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0]
try:
package, module = tokens[2].split('.')
except ValueError: # unpack list of wrong size
raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0]
try:
content_type = ContentType.objects.get(app_label__exact=package, model__exact=module)
except ContentType.DoesNotExist:
raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module)
obj_id_lookup_var, obj_id = None, None
if tokens[3].isdigit():
obj_id = tokens[3]
try: # ensure the object ID is valid
content_type.get_object_for_this_type(pk=obj_id)
except ObjectDoesNotExist:
raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id)
else:
obj_id_lookup_var = tokens[3]
kwargs = {}
if len(tokens) > 4:
if tokens[4] != 'with':
raise template.TemplateSyntaxError, "Fourth argument in %r tag must be 'with'" % tokens[0]
for option, args in zip(tokens[5::2], tokens[6::2]):
option = smart_str(option)
if option in ('photos_optional', 'photos_required') and not self.free:
# VALIDATION ##############################################
option_list = args.split(',')
if len(option_list) % 3 != 0:
raise template.TemplateSyntaxError, "Incorrect number of comma-separated arguments to %r tag" % tokens[0]
for opt in option_list[::3]:
if not opt.isalnum():
raise template.TemplateSyntaxError, "Invalid photo directory name in %r tag: '%s'" % (tokens[0], opt)
for opt in option_list[1::3] + option_list[2::3]:
if not opt.isdigit() or not (MIN_PHOTO_DIMENSION <= int(opt) <= MAX_PHOTO_DIMENSION):
raise template.TemplateSyntaxError, "Invalid photo dimension in %r tag: '%s'. Only values between %s and %s are allowed." % (tokens[0], opt, MIN_PHOTO_DIMENSION, MAX_PHOTO_DIMENSION)
# VALIDATION ENDS #########################################
kwargs[option] = True
kwargs['photo_options'] = args
elif option in ('ratings_optional', 'ratings_required') and not self.free:
# VALIDATION ##############################################
if 2 < len(args.split('|')) > 9:
raise template.TemplateSyntaxError, "Incorrect number of '%s' options in %r tag. Use between 2 and 8." % (option, tokens[0])
if re.match('^scale:\d+\-\d+\:$', args.split('|')[0]):
raise template.TemplateSyntaxError, "Invalid 'scale' in %r tag's '%s' options" % (tokens[0], option)
# VALIDATION ENDS #########################################
kwargs[option] = True
kwargs['rating_options'] = args
elif option in ('is_public'):
kwargs[option] = (args == 'true')
else:
raise template.TemplateSyntaxError, "%r tag got invalid parameter '%s'" % (tokens[0], option)
return CommentFormNode(content_type, obj_id_lookup_var, obj_id, self.free, **kwargs)
class DoCommentCount:
#@register.tag
def get_comment_list(parser, token):
"""
Gets comment count for the given params and populates the template context
with a variable containing that value, whose name is defined by the 'as'
clause.
Gets the list of comments for the given params and populates the template
context with a variable containing that value, whose name is defined by the
'as' clause.
Syntax::
{% get_comment_count for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname] %}
{% get_comment_list for [object] as [varname] %}
{% get_comment_list for [app].[model] [object_id] as [varname] %}
Example usage::
{% get_comment_count for lcom.eventtimes event.id as comment_count %}
{% get_comment_list for event as comment_list %}
{% for comment in comment_list %}
...
{% endfor %}
Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this::
{% get_comment_count for lcom.eventtimes 23 as comment_count %}
"""
def __init__(self, free):
self.free = free
return CommentListNode.handle_token(parser, token)
def __call__(self, parser, token):
tokens = token.contents.split()
# Now tokens is a list like this:
# ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list']
if len(tokens) != 6:
raise template.TemplateSyntaxError, "%r tag requires 5 arguments" % tokens[0]
if tokens[1] != 'for':
raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0]
try:
package, module = tokens[2].split('.')
except ValueError: # unpack list of wrong size
raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0]
try:
content_type = ContentType.objects.get(app_label__exact=package, model__exact=module)
except ContentType.DoesNotExist:
raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module)
var_name, obj_id = None, None
if tokens[3].isdigit():
obj_id = tokens[3]
try: # ensure the object ID is valid
content_type.get_object_for_this_type(pk=obj_id)
except ObjectDoesNotExist:
raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id)
else:
var_name = tokens[3]
if tokens[4] != 'as':
raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0]
return CommentCountNode(package, module, var_name, obj_id, tokens[5], self.free)
class DoGetCommentList:
#@register.tag
def get_comment_form(parser, token):
"""
Gets comments for the given params and populates the template context with a
special comment_package variable, whose name is defined by the ``as``
clause.
Get a (new) form object to post a new comment.
Syntax::
{% get_comment_list for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname] (reversed) %}
Example usage::
{% get_comment_list for lcom.eventtimes event.id as comment_list %}
Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this::
{% get_comment_list for lcom.eventtimes 23 as comment_list %}
To get a list of comments in reverse order -- that is, most recent first --
pass ``reversed`` as the last param::
{% get_comment_list for lcom.eventtimes event.id as comment_list reversed %}
{% get_comment_form for [object] as [varname] %}
{% get_comment_form for [app].[model] [object_id] as [varname] %}
"""
def __init__(self, free):
self.free = free
return CommentFormNode.handle_token(parser, token)
def __call__(self, parser, token):
tokens = token.contents.split()
# Now tokens is a list like this:
# ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list']
if not len(tokens) in (6, 7):
raise template.TemplateSyntaxError, "%r tag requires 5 or 6 arguments" % tokens[0]
if tokens[1] != 'for':
raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0]
try:
package, module = tokens[2].split('.')
except ValueError: # unpack list of wrong size
raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0]
try:
content_type = ContentType.objects.get(app_label__exact=package,model__exact=module)
except ContentType.DoesNotExist:
raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module)
var_name, obj_id = None, None
if tokens[3].isdigit():
obj_id = tokens[3]
try: # ensure the object ID is valid
content_type.get_object_for_this_type(pk=obj_id)
except ObjectDoesNotExist:
raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id)
else:
var_name = tokens[3]
if tokens[4] != 'as':
raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0]
if len(tokens) == 7:
if tokens[6] != 'reversed':
raise template.TemplateSyntaxError, "Final argument in %r must be 'reversed' if given" % tokens[0]
ordering = "-"
else:
ordering = ""
return CommentListNode(package, module, var_name, obj_id, tokens[5], self.free, ordering)
#@register.tag
def render_comment_form(parser, token):
"""
Render the comment form (as returned by ``{% render_comment_form %}``) through
the ``comments/form.html`` template.
# registration comments
register.tag('get_comment_list', DoGetCommentList(False))
register.tag('comment_form', DoCommentForm(False))
register.tag('get_comment_count', DoCommentCount(False))
# free comments
register.tag('get_free_comment_list', DoGetCommentList(True))
register.tag('free_comment_form', DoCommentForm(True))
register.tag('get_free_comment_count', DoCommentCount(True))
Syntax::
{% render_comment_form for [object] %}
{% render_comment_form for [app].[model] [object_id] %}
"""
return RenderCommentFormNode.handle_token(parser, token)
#@register.simple_tag
def comment_form_target():
"""
Get the target URL for the comment form.
Example::
<form action="{% comment_form_target %}" method="POST">
"""
return comments.get_form_target()
register.tag(get_comment_count)
register.tag(get_comment_list)
register.tag(get_comment_form)
register.tag(render_comment_form)
register.simple_tag(comment_form_target)

View File

@ -1,13 +0,0 @@
# coding: utf-8
r"""
>>> from django.contrib.comments.models import Comment
>>> from django.contrib.auth.models import User
>>> u = User.objects.create_user('commenttestuser', 'commenttest@example.com', 'testpw')
>>> c = Comment(user=u, comment=u'\xe2')
>>> c
<Comment: commenttestuser: â...>
>>> print c
commenttestuser: â...
"""

View File

@ -0,0 +1,15 @@
from django.conf.urls.defaults import *
from django.conf import settings
urlpatterns = patterns('django.contrib.comments.views',
url(r'^post/$', 'comments.post_comment', name='comments-post-comment'),
url(r'^posted/$', 'comments.comment_done', name='comments-comment-done'),
url(r'^flag/(\d+)/$', 'moderation.flag', name='comments-flag'),
url(r'^flagged/$', 'moderation.flag_done', name='comments-flag-done'),
url(r'^delete/(\d+)/$', 'moderation.delete', name='comments-delete'),
url(r'^deleted/$', 'moderation.delete_done', name='comments-delete-done'),
url(r'^moderate/$', 'moderation.moderation_queue', name='comments-moderation-queue'),
url(r'^approve/(\d+)/$', 'moderation.approve', name='comments-approve'),
url(r'^approved/$', 'moderation.approve_done', name='comments-approve-done'),
)

View File

@ -1,12 +0,0 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('django.contrib.comments.views',
(r'^post/$', 'comments.post_comment'),
(r'^postfree/$', 'comments.post_free_comment'),
(r'^posted/$', 'comments.comment_was_posted'),
(r'^karma/vote/(?P<comment_id>\d+)/(?P<vote>up|down)/$', 'karma.vote'),
(r'^flag/(?P<comment_id>\d+)/$', 'userflags.flag'),
(r'^flag/(?P<comment_id>\d+)/done/$', 'userflags.flag_done'),
(r'^delete/(?P<comment_id>\d+)/$', 'userflags.delete'),
(r'^delete/(?P<comment_id>\d+)/done/$', 'userflags.delete_done'),
)

View File

@ -1,393 +1,116 @@
import base64
import datetime
from django.core import validators
from django import oldforms
from django.core.mail import mail_admins, mail_managers
from django.http import Http404
from django import http
from django.conf import settings
from utils import next_redirect, confirmation_view
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.contrib.comments.models import Comment, FreeComment, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import authenticate
from django.http import HttpResponseRedirect
from django.utils.text import normalize_newlines
from django.conf import settings
from django.utils.translation import ungettext, ugettext as _
from django.utils.encoding import smart_unicode
from django.template.loader import render_to_string
from django.utils.html import escape
from django.contrib import comments
from django.contrib.comments import signals
COMMENTS_PER_PAGE = 20
# TODO: This is a copy of the manipulator-based form that used to live in
# contrib.auth.forms. It should be replaced with the newforms version that
# has now been added to contrib.auth.forms when the comments app gets updated
# for newforms.
class AuthenticationForm(oldforms.Manipulator):
class CommentPostBadRequest(http.HttpResponseBadRequest):
"""
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
Response returned when a comment post is invalid. If ``DEBUG`` is on a
nice-ish error message will be displayed (for debugging purposes), but in
production mode a simple opaque 400 page will be displayed.
"""
def __init__(self, request=None):
"""
If request is passed in, the manipulator will validate that cookies are
enabled. Note that the request (a HttpRequest object) must have set a
cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before
running this validator.
"""
self.request = request
self.fields = [
oldforms.TextField(field_name="username", length=15, max_length=30, is_required=True,
validator_list=[self.isValidUser, self.hasCookiesEnabled]),
oldforms.PasswordField(field_name="password", length=15, max_length=30, is_required=True),
def __init__(self, why):
super(CommentPostBadRequest, self).__init__()
if settings.DEBUG:
self.content = render_to_string("comments/400-debug.html", {"why": why})
def post_comment(request, next=None):
"""
Post a comment.
HTTP POST is required. If ``POST['submit'] == "preview"`` or if there are
errors a preview template, ``comments/preview.html``, will be rendered.
"""
# Require POST
if request.method != 'POST':
return http.HttpResponseNotAllowed(["POST"])
# Fill out some initial data fields from an authenticated user, if present
data = request.POST.copy()
if request.user.is_authenticated():
if "name" not in data:
data["name"] = request.user.get_full_name()
if "email" not in data:
data["email"] = request.user.email
# Look up the object we're trying to comment about
ctype = data.get("content_type")
object_pk = data.get("object_pk")
if ctype is None or object_pk is None:
return CommentPostBadRequest("Missing content_type or object_pk field.")
try:
model = models.get_model(*ctype.split(".", 1))
target = model._default_manager.get(pk=object_pk)
except TypeError:
return CommentPostBadRequest(
"Invalid content_type value: %r" % escape(ctype))
except AttributeError:
return CommentPostBadRequest(
"The given content-type %r does not resolve to a valid model." % \
escape(ctype))
except ObjectDoesNotExist:
return CommentPostBadRequest(
"No object matching content-type %r and object PK %r exists." % \
(escape(ctype), escape(object_pk)))
# Do we want to preview the comment?
preview = data.get("submit", "").lower() == "preview" or \
data.get("preview", None) is not None
# Construct the comment form
form = comments.get_form()(target, data=data)
# Check security information
if form.security_errors():
return CommentPostBadRequest(
"The comment form failed security verification: %s" % \
escape(str(form.security_errors())))
# If there are errors or if we requested a preview show the comment
if form.errors or preview:
template_list = [
"comments/%s_%s_preview.html" % tuple(str(model._meta).split(".")),
"comments/%s_preview.html" % model._meta.app_label,
"comments/preview.html",
]
self.user_cache = None
def hasCookiesEnabled(self, field_data, all_data):
if self.request and not self.request.session.test_cookie_worked():
raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
def isValidUser(self, field_data, all_data):
username = field_data
password = all_data.get('password', None)
self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None:
raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
elif not self.user_cache.is_active:
raise validators.ValidationError, _("This account is inactive.")
def get_user_id(self):
if self.user_cache:
return self.user_cache.id
return None
def get_user(self):
return self.user_cache
class PublicCommentManipulator(AuthenticationForm):
"Manipulator that handles public registered comments"
def __init__(self, user, ratings_required, ratings_range, num_rating_choices):
AuthenticationForm.__init__(self)
self.ratings_range, self.num_rating_choices = ratings_range, num_rating_choices
choices = [(c, c) for c in ratings_range]
def get_validator_list(rating_num):
if rating_num <= num_rating_choices:
return [validators.RequiredIfOtherFieldsGiven(['rating%d' % i for i in range(1, 9) if i != rating_num], _("This rating is required because you've entered at least one other rating."))]
else:
return []
self.fields.extend([
oldforms.LargeTextField(field_name="comment", max_length=3000, is_required=True,
validator_list=[self.hasNoProfanities]),
oldforms.RadioSelectField(field_name="rating1", choices=choices,
is_required=ratings_required and num_rating_choices > 0,
validator_list=get_validator_list(1),
),
oldforms.RadioSelectField(field_name="rating2", choices=choices,
is_required=ratings_required and num_rating_choices > 1,
validator_list=get_validator_list(2),
),
oldforms.RadioSelectField(field_name="rating3", choices=choices,
is_required=ratings_required and num_rating_choices > 2,
validator_list=get_validator_list(3),
),
oldforms.RadioSelectField(field_name="rating4", choices=choices,
is_required=ratings_required and num_rating_choices > 3,
validator_list=get_validator_list(4),
),
oldforms.RadioSelectField(field_name="rating5", choices=choices,
is_required=ratings_required and num_rating_choices > 4,
validator_list=get_validator_list(5),
),
oldforms.RadioSelectField(field_name="rating6", choices=choices,
is_required=ratings_required and num_rating_choices > 5,
validator_list=get_validator_list(6),
),
oldforms.RadioSelectField(field_name="rating7", choices=choices,
is_required=ratings_required and num_rating_choices > 6,
validator_list=get_validator_list(7),
),
oldforms.RadioSelectField(field_name="rating8", choices=choices,
is_required=ratings_required and num_rating_choices > 7,
validator_list=get_validator_list(8),
),
])
if user.is_authenticated():
self["username"].is_required = False
self["username"].validator_list = []
self["password"].is_required = False
self["password"].validator_list = []
self.user_cache = user
def hasNoProfanities(self, field_data, all_data):
if settings.COMMENTS_ALLOW_PROFANITIES:
return
return validators.hasNoProfanities(field_data, all_data)
def get_comment(self, new_data):
"Helper function"
return Comment(None, self.get_user_id(), new_data["content_type_id"],
new_data["object_id"], new_data.get("headline", "").strip(),
new_data["comment"].strip(), new_data.get("rating1", None),
new_data.get("rating2", None), new_data.get("rating3", None),
new_data.get("rating4", None), new_data.get("rating5", None),
new_data.get("rating6", None), new_data.get("rating7", None),
new_data.get("rating8", None), new_data.get("rating1", None) is not None,
datetime.datetime.now(), new_data["is_public"], new_data["ip_address"], False, settings.SITE_ID)
def save(self, new_data):
today = datetime.date.today()
c = self.get_comment(new_data)
for old in Comment.objects.filter(content_type__id__exact=new_data["content_type_id"],
object_id__exact=new_data["object_id"], user__id__exact=self.get_user_id()):
# Check that this comment isn't duplicate. (Sometimes people post
# comments twice by mistake.) If it is, fail silently by pretending
# the comment was posted successfully.
if old.submit_date.date() == today and old.comment == c.comment \
and old.rating1 == c.rating1 and old.rating2 == c.rating2 \
and old.rating3 == c.rating3 and old.rating4 == c.rating4 \
and old.rating5 == c.rating5 and old.rating6 == c.rating6 \
and old.rating7 == c.rating7 and old.rating8 == c.rating8:
return old
# If the user is leaving a rating, invalidate all old ratings.
if c.rating1 is not None:
old.valid_rating = False
old.save()
c.save()
# If the commentor has posted fewer than COMMENTS_FIRST_FEW comments,
# send the comment to the managers.
if self.user_cache.comment_set.count() <= settings.COMMENTS_FIRST_FEW:
message = ungettext('This comment was posted by a user who has posted fewer than %(count)s comment:\n\n%(text)s',
'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s', settings.COMMENTS_FIRST_FEW) % \
{'count': settings.COMMENTS_FIRST_FEW, 'text': c.get_as_text()}
mail_managers("Comment posted by rookie user", message)
if settings.COMMENTS_SKETCHY_USERS_GROUP and settings.COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.groups.all()]:
message = _('This comment was posted by a sketchy user:\n\n%(text)s') % {'text': c.get_as_text()}
mail_managers("Comment posted by sketchy user (%s)" % self.user_cache.username, c.get_as_text())
return c
class PublicFreeCommentManipulator(oldforms.Manipulator):
"Manipulator that handles public free (unregistered) comments"
def __init__(self):
self.fields = (
oldforms.TextField(field_name="person_name", max_length=50, is_required=True,
validator_list=[self.hasNoProfanities]),
oldforms.LargeTextField(field_name="comment", max_length=3000, is_required=True,
validator_list=[self.hasNoProfanities]),
return render_to_response(
template_list, {
"comment" : form.data.get("comment", ""),
"form" : form,
},
RequestContext(request, {})
)
def hasNoProfanities(self, field_data, all_data):
if settings.COMMENTS_ALLOW_PROFANITIES:
return
return validators.hasNoProfanities(field_data, all_data)
# Otherwise create the comment
comment = form.get_comment_object()
comment.ip_address = request.META.get("REMOTE_ADDR", None)
if request.user.is_authenticated():
comment.user = request.user
def get_comment(self, new_data):
"Helper function"
return FreeComment(None, new_data["content_type_id"],
new_data["object_id"], new_data["comment"].strip(),
new_data["person_name"].strip(), datetime.datetime.now(), new_data["is_public"],
new_data["ip_address"], False, settings.SITE_ID)
# Signal that the comment is about to be saved
responses = signals.comment_will_be_posted.send(comment)
def save(self, new_data):
today = datetime.date.today()
c = self.get_comment(new_data)
# Check that this comment isn't duplicate. (Sometimes people post
# comments twice by mistake.) If it is, fail silently by pretending
# the comment was posted successfully.
for old_comment in FreeComment.objects.filter(content_type__id__exact=new_data["content_type_id"],
object_id__exact=new_data["object_id"], person_name__exact=new_data["person_name"],
submit_date__year=today.year, submit_date__month=today.month,
submit_date__day=today.day):
if old_comment.comment == c.comment:
return old_comment
c.save()
return c
for (receiver, response) in responses:
if response == False:
return CommentPostBadRequest(
"comment_will_be_posted receiver %r killed the comment" % receiver.__name__)
def post_comment(request, extra_context=None, context_processors=None):
"""
Post a comment
# Save the comment and signal that it was saved
comment.save()
signals.comment_was_posted.send(comment)
Redirects to the `comments.comments.comment_was_posted` view upon success.
return next_redirect(data, next, comment_done, c=comment._get_pk_val())
Templates: `comment_preview`
Context:
comment
the comment being posted
comment_form
the comment form
options
comment options
target
comment target
hash
security hash (must be included in a posted form to succesfully
post a comment).
rating_options
comment ratings options
ratings_optional
are ratings optional?
ratings_required
are ratings required?
rating_range
range of ratings
rating_choices
choice of ratings
"""
if extra_context is None: extra_context = {}
if not request.POST:
raise Http404, _("Only POSTs are allowed")
try:
options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo']
except KeyError:
raise Http404, _("One or more of the required fields wasn't submitted")
photo_options = request.POST.get('photo_options', '')
rating_options = normalize_newlines(request.POST.get('rating_options', ''))
if Comment.objects.get_security_hash(options, photo_options, rating_options, target) != security_hash:
raise Http404, _("Somebody tampered with the comment form (security violation)")
# Now we can be assured the data is valid.
if rating_options:
rating_range, rating_choices = Comment.objects.get_rating_options(base64.decodestring(rating_options))
else:
rating_range, rating_choices = [], []
content_type_id, object_id = target.split(':') # target is something like '52:5157'
try:
obj = ContentType.objects.get(pk=content_type_id).get_object_for_this_type(pk=object_id)
except ObjectDoesNotExist:
raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid")
option_list = options.split(',') # options is something like 'pa,ra'
new_data = request.POST.copy()
new_data['content_type_id'] = content_type_id
new_data['object_id'] = object_id
new_data['ip_address'] = request.META.get('REMOTE_ADDR')
new_data['is_public'] = IS_PUBLIC in option_list
manipulator = PublicCommentManipulator(request.user,
ratings_required=RATINGS_REQUIRED in option_list,
ratings_range=rating_range,
num_rating_choices=len(rating_choices))
errors = manipulator.get_validation_errors(new_data)
# If user gave correct username/password and wasn't already logged in, log them in
# so they don't have to enter a username/password again.
if manipulator.get_user() and not manipulator.get_user().is_authenticated() and 'password' in new_data and manipulator.get_user().check_password(new_data['password']):
from django.contrib.auth import login
login(request, manipulator.get_user())
if errors or 'preview' in request.POST:
class CommentFormWrapper(oldforms.FormWrapper):
def __init__(self, manipulator, new_data, errors, rating_choices):
oldforms.FormWrapper.__init__(self, manipulator, new_data, errors)
self.rating_choices = rating_choices
def ratings(self):
field_list = [self['rating%d' % (i+1)] for i in range(len(rating_choices))]
for i, f in enumerate(field_list):
f.choice = rating_choices[i]
return field_list
comment = errors and '' or manipulator.get_comment(new_data)
comment_form = CommentFormWrapper(manipulator, new_data, errors, rating_choices)
return render_to_response('comments/preview.html', {
'comment': comment,
'comment_form': comment_form,
'options': options,
'target': target,
'hash': security_hash,
'rating_options': rating_options,
'ratings_optional': RATINGS_OPTIONAL in option_list,
'ratings_required': RATINGS_REQUIRED in option_list,
'rating_range': rating_range,
'rating_choices': rating_choices,
}, context_instance=RequestContext(request, extra_context, context_processors))
elif 'post' in request.POST:
# If the IP is banned, mail the admins, do NOT save the comment, and
# serve up the "Thanks for posting" page as if the comment WAS posted.
if request.META['REMOTE_ADDR'] in settings.BANNED_IPS:
mail_admins("Banned IP attempted to post comment", smart_unicode(request.POST) + "\n\n" + str(request.META))
else:
manipulator.do_html2python(new_data)
comment = manipulator.save(new_data)
return HttpResponseRedirect("../posted/?c=%s:%s" % (content_type_id, object_id))
else:
raise Http404, _("The comment form didn't provide either 'preview' or 'post'")
comment_done = confirmation_view(
template = "comments/posted.html",
doc = """Display a "comment was posted" success page."""
)
def post_free_comment(request, extra_context=None, context_processors=None):
"""
Post a free comment (not requiring a log in)
Redirects to `comments.comments.comment_was_posted` view on success.
Templates: `comment_free_preview`
Context:
comment
comment being posted
comment_form
comment form object
options
comment options
target
comment target
hash
security hash (must be included in a posted form to succesfully
post a comment).
"""
if extra_context is None: extra_context = {}
if not request.POST:
raise Http404, _("Only POSTs are allowed")
try:
options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo']
except KeyError:
raise Http404, _("One or more of the required fields wasn't submitted")
if Comment.objects.get_security_hash(options, '', '', target) != security_hash:
raise Http404, _("Somebody tampered with the comment form (security violation)")
content_type_id, object_id = target.split(':') # target is something like '52:5157'
content_type = ContentType.objects.get(pk=content_type_id)
try:
obj = content_type.get_object_for_this_type(pk=object_id)
except ObjectDoesNotExist:
raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid")
option_list = options.split(',')
new_data = request.POST.copy()
new_data['content_type_id'] = content_type_id
new_data['object_id'] = object_id
new_data['ip_address'] = request.META['REMOTE_ADDR']
new_data['is_public'] = IS_PUBLIC in option_list
manipulator = PublicFreeCommentManipulator()
errors = manipulator.get_validation_errors(new_data)
if errors or 'preview' in request.POST:
comment = errors and '' or manipulator.get_comment(new_data)
return render_to_response('comments/free_preview.html', {
'comment': comment,
'comment_form': oldforms.FormWrapper(manipulator, new_data, errors),
'options': options,
'target': target,
'hash': security_hash,
}, context_instance=RequestContext(request, extra_context, context_processors))
elif 'post' in request.POST:
# If the IP is banned, mail the admins, do NOT save the comment, and
# serve up the "Thanks for posting" page as if the comment WAS posted.
if request.META['REMOTE_ADDR'] in settings.BANNED_IPS:
from django.core.mail import mail_admins
mail_admins("Practical joker", smart_unicode(request.POST) + "\n\n" + str(request.META))
else:
manipulator.do_html2python(new_data)
comment = manipulator.save(new_data)
return HttpResponseRedirect("../posted/?c=%s:%s" % (content_type_id, object_id))
else:
raise Http404, _("The comment form didn't provide either 'preview' or 'post'")
def comment_was_posted(request, extra_context=None, context_processors=None):
"""
Display "comment was posted" success page
Templates: `comment_posted`
Context:
object
The object the comment was posted on
"""
if extra_context is None: extra_context = {}
obj = None
if 'c' in request.GET:
content_type_id, object_id = request.GET['c'].split(':')
try:
content_type = ContentType.objects.get(pk=content_type_id)
obj = content_type.get_object_for_this_type(pk=object_id)
except ObjectDoesNotExist:
pass
return render_to_response('comments/posted.html', {'object': obj},
context_instance=RequestContext(request, extra_context, context_processors))

View File

@ -1,32 +0,0 @@
from django.http import Http404
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.contrib.comments.models import Comment, KarmaScore
from django.utils.translation import ugettext as _
def vote(request, comment_id, vote, extra_context=None, context_processors=None):
"""
Rate a comment (+1 or -1)
Templates: `karma_vote_accepted`
Context:
comment
`comments.comments` object being rated
"""
if extra_context is None: extra_context = {}
rating = {'up': 1, 'down': -1}.get(vote, False)
if not rating:
raise Http404, "Invalid vote"
if not request.user.is_authenticated():
raise Http404, _("Anonymous users cannot vote")
try:
comment = Comment.objects.get(pk=comment_id)
except Comment.DoesNotExist:
raise Http404, _("Invalid comment ID")
if comment.user.id == request.user.id:
raise Http404, _("No voting for yourself")
KarmaScore.objects.vote(request.user.id, comment_id, rating)
# Reload comment to ensure we have up to date karma count
comment = Comment.objects.get(pk=comment_id)
return render_to_response('comments/karma_vote_accepted.html', {'comment': comment},
context_instance=RequestContext(request, extra_context, context_processors))

View File

@ -0,0 +1,186 @@
from django import template
from django.conf import settings
from django.shortcuts import get_object_or_404, render_to_response
from django.contrib.auth.decorators import login_required, permission_required
from utils import next_redirect, confirmation_view
from django.core.paginator import Paginator, InvalidPage
from django.http import Http404
from django.contrib import comments
from django.contrib.comments import signals
#@login_required
def flag(request, comment_id, next=None):
"""
Flags a comment. Confirmation on GET, action on POST.
Templates: `comments/flag.html`,
Context:
comment
the flagged `comments.comment` object
"""
comment = get_object_or_404(comments.get_model(), pk=comment_id, site__pk=settings.SITE_ID)
# Flag on POST
if request.method == 'POST':
flag, created = comments.models.CommentFlag.objects.get_or_create(
comment = comment,
user = request.user,
flag = comments.models.CommentFlag.SUGGEST_REMOVAL
)
signals.comment_was_flagged.send(comment)
return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
# Render a form on GET
else:
return render_to_response('comments/flag.html',
{'comment': comment, "next": next},
template.RequestContext(request)
)
flag = login_required(flag)
#@permission_required("comments.delete_comment")
def delete(request, comment_id, next=None):
"""
Deletes a comment. Confirmation on GET, action on POST. Requires the "can
moderate comments" permission.
Templates: `comments/delete.html`,
Context:
comment
the flagged `comments.comment` object
"""
comment = get_object_or_404(comments.get_model(), pk=comment_id, site__pk=settings.SITE_ID)
# Delete on POST
if request.method == 'POST':
# Flag the comment as deleted instead of actually deleting it.
flag, created = comments.models.CommentFlag.objects.get_or_create(
comment = comment,
user = request.user,
flag = comments.models.CommentFlag.MODERATOR_DELETION
)
comment.is_removed = True
comment.save()
signals.comment_was_flagged.send(comment)
return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
# Render a form on GET
else:
return render_to_response('comments/delete.html',
{'comment': comment, "next": next},
template.RequestContext(request)
)
delete = permission_required("comments.can_moderate")(delete)
#@permission_required("comments.can_moderate")
def approve(request, comment_id, next=None):
"""
Approve a comment (that is, mark it as public and non-removed). Confirmation
on GET, action on POST. Requires the "can moderate comments" permission.
Templates: `comments/approve.html`,
Context:
comment
the `comments.comment` object for approval
"""
comment = get_object_or_404(comments.get_model(), pk=comment_id, site__pk=settings.SITE_ID)
# Delete on POST
if request.method == 'POST':
# Flag the comment as approved.
flag, created = comments.models.CommentFlag.objects.get_or_create(
comment = comment,
user = request.user,
flag = comments.models.CommentFlag.MODERATOR_APPROVAL,
)
comment.is_removed = False
comment.is_public = True
comment.save()
signals.comment_was_flagged.send(comment)
return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
# Render a form on GET
else:
return render_to_response('comments/approve.html',
{'comment': comment, "next": next},
template.RequestContext(request)
)
approve = permission_required("comments.can_moderate")(approve)
#@permission_required("comments.can_moderate")
def moderation_queue(request):
"""
Displays a list of unapproved comments to be approved.
Templates: `comments/moderation_queue.html`
Context:
comments
Comments to be approved (paginated).
empty
Is the comment list empty?
is_paginated
Is there more than one page?
results_per_page
Number of comments per page
has_next
Is there a next page?
has_previous
Is there a previous page?
page
The current page number
next
The next page number
pages
Number of pages
hits
Total number of comments
page_range
Range of page numbers
"""
qs = comments.get_model().objects.filter(is_public=False, is_removed=False)
paginator = Paginator(qs, 100)
try:
page = int(request.GET.get("page", 1))
except ValueError:
raise Http404
try:
comments_per_page = paginator.page(page)
except InvalidPage:
raise Http404
return render_to_response("comments/moderation_queue.html", {
'comments' : comments_per_page.object_list,
'empty' : page == 1 and paginator.count == 0,
'is_paginated': paginator.num_pages > 1,
'results_per_page': 100,
'has_next': comments_per_page.has_next(),
'has_previous': comments_per_page.has_previous(),
'page': page,
'next': page + 1,
'previous': page - 1,
'pages': paginator.num_pages,
'hits' : paginator.count,
'page_range' : paginator.page_range
}, context_instance=template.RequestContext(request))
moderation_queue = permission_required("comments.can_moderate")(moderation_queue)
flag_done = confirmation_view(
template = "comments/flagged.html",
doc = 'Displays a "comment was flagged" success page.'
)
delete_done = confirmation_view(
template = "comments/deleted.html",
doc = 'Displays a "comment was deleted" success page.'
)
approve_done = confirmation_view(
template = "comments/approved.html",
doc = 'Displays a "comment was approved" success page.'
)

View File

@ -1,62 +0,0 @@
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django.http import Http404
from django.contrib.comments.models import Comment, ModeratorDeletion, UserFlag
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect
from django.conf import settings
def flag(request, comment_id, extra_context=None, context_processors=None):
"""
Flags a comment. Confirmation on GET, action on POST.
Templates: `comments/flag_verify`, `comments/flag_done`
Context:
comment
the flagged `comments.comments` object
"""
if extra_context is None: extra_context = {}
comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID)
if request.POST:
UserFlag.objects.flag(comment, request.user)
return HttpResponseRedirect('%sdone/' % request.path)
return render_to_response('comments/flag_verify.html', {'comment': comment},
context_instance=RequestContext(request, extra_context, context_processors))
flag = login_required(flag)
def flag_done(request, comment_id, extra_context=None, context_processors=None):
if extra_context is None: extra_context = {}
comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID)
return render_to_response('comments/flag_done.html', {'comment': comment},
context_instance=RequestContext(request, extra_context, context_processors))
def delete(request, comment_id, extra_context=None, context_processors=None):
"""
Deletes a comment. Confirmation on GET, action on POST.
Templates: `comments/delete_verify`, `comments/delete_done`
Context:
comment
the flagged `comments.comments` object
"""
if extra_context is None: extra_context = {}
comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID)
if not Comment.objects.user_is_moderator(request.user):
raise Http404
if request.POST:
# If the comment has already been removed, silently fail.
if not comment.is_removed:
comment.is_removed = True
comment.save()
m = ModeratorDeletion(None, request.user.id, comment.id, None)
m.save()
return HttpResponseRedirect('%sdone/' % request.path)
return render_to_response('comments/delete_verify.html', {'comment': comment},
context_instance=RequestContext(request, extra_context, context_processors))
delete = login_required(delete)
def delete_done(request, comment_id, extra_context=None, context_processors=None):
if extra_context is None: extra_context = {}
comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID)
return render_to_response('comments/delete_done.html', {'comment': comment},
context_instance=RequestContext(request, extra_context, context_processors))

View File

@ -0,0 +1,58 @@
"""
A few bits of helper functions for comment views.
"""
import urllib
import textwrap
from django.http import HttpResponseRedirect
from django.core import urlresolvers
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.contrib import comments
def next_redirect(data, default, default_view, **get_kwargs):
"""
Handle the "where should I go next?" part of comment views.
The next value could be a kwarg to the function (``default``), or a
``?next=...`` GET arg, or the URL of a given view (``default_view``). See
the view modules for examples.
Returns an ``HttpResponseRedirect``.
"""
next = data.get("next", default)
if next is None:
next = urlresolvers.reverse(default_view)
if get_kwargs:
next += "?" + urllib.urlencode(get_kwargs)
return HttpResponseRedirect(next)
def confirmation_view(template, doc="Display a confirmation view."):
"""
Confirmation view generator for the "comment was
posted/flagged/deleted/approved" views.
"""
def confirmed(request):
comment = None
if 'c' in request.GET:
try:
comment = comments.get_model().objects.get(pk=request.GET['c'])
except ObjectDoesNotExist:
pass
return render_to_response(template,
{'comment': comment},
context_instance=RequestContext(request)
)
confirmed.__doc__ = textwrap.dedent("""\
%s
Templates: `%s``
Context:
comment
The posted comment
""" % (help, template)
)
return confirmed

View File

@ -62,7 +62,7 @@ ins { font-weight: bold; text-decoration: none; }
/*** lists ***/
ul { padding-left:30px; }
ol { padding-left:30px; }
ol.arabic { list-style-type: decimal; }
ol.arabic li { list-style-type: decimal; }
ul li { list-style-type:square; margin-bottom:.4em; }
ol li { margin-bottom: .4em; }
ul ul { padding-left:1.2em; }

View File

@ -72,10 +72,16 @@ Using Django
And more:
---------
:ref:`topics-auth` ... :ref:`topics-cache` ... :ref:`topics-email` ...
:ref:`topics-files` ... :ref:`topics-i18n` ... :ref:`topics-install` ...
:ref:`topics-pagination` ... :ref:`topics-serialization` ...
:ref:`topics-settings` ... :ref:`topics-testing`
* :ref:`topics-auth`
* :ref:`topics-cache`
* :ref:`topics-email`
* :ref:`topics-files`
* :ref:`topics-i18n`
* :ref:`topics-install`
* :ref:`topics-pagination`
* :ref:`topics-serialization`
* :ref:`topics-settings`
* :ref:`topics-testing`
Add-on ("contrib") applications
===============================
@ -95,11 +101,16 @@ Add-on ("contrib") applications
And more:
---------
:ref:`ref-contrib-contenttypes` ... :ref:`ref-contrib-csrf` ...
:ref:`ref-contrib-databrowse` ... :ref:`ref-contrib-flatpages` ...
:ref:`ref-contrib-humanize` ... :ref:`ref-contrib-redirects` ...
:ref:`ref-contrib-sitemaps` ... :ref:`ref-contrib-sites` ...
:ref:`ref-contrib-webdesign`
* :ref:`ref-contrib-comments-index`
* :ref:`ref-contrib-contenttypes`
* :ref:`ref-contrib-csrf`
* :ref:`ref-contrib-databrowse`
* :ref:`ref-contrib-flatpages`
* :ref:`ref-contrib-humanize`
* :ref:`ref-contrib-redirects`
* :ref:`ref-contrib-sitemaps`
* :ref:`ref-contrib-sites`
* :ref:`ref-contrib-webdesign`
Solving specific problems
=========================
@ -120,11 +131,14 @@ Solving specific problems
And more:
---------
:ref:`Authenticating in Apache <howto-apache-auth>` ...
:ref:`howto-custom-file-storage` ... :ref:`howto-custom-management-commands` ...
:ref:`howto-custom-model-fields` ... :ref:`howto-error-reporting` ...
:ref:`howto-initial-data` ... :ref:`howto-static-files`
* :ref:`Authenticating in Apache <howto-apache-auth>`
* :ref:`howto-custom-file-storage`
* :ref:`howto-custom-management-commands`
* :ref:`howto-custom-model-fields`
* :ref:`howto-error-reporting`
* :ref:`howto-initial-data`
* :ref:`howto-static-files`
Reference
=========
@ -143,9 +157,13 @@ Reference
And more:
---------
:ref:`ref-databases` ... :ref:`ref-django-admin` ... :ref:`ref-files-index` ...
:ref:`ref-generic-views` ... :ref:`ref-middleware` ...
:ref:`ref-templates-index` ... :ref:`ref-unicode`
* :ref:`ref-databases`
* :ref:`ref-django-admin`
* :ref:`ref-files-index`
* :ref:`ref-generic-views`
* :ref:`ref-middleware`
* :ref:`ref-templates-index`
* :ref:`ref-unicode`
And all the rest
================

View File

@ -0,0 +1,212 @@
.. _ref-contrib-comments-index:
===========================
Django's comments framework
===========================
.. module:: django.contrib.comments
:synopsis: Django's comment framework
Django includes a simple, yet customizable comments framework. The built-in
comments framework can be used to attach comments to any model, so you can use
it for comments on blog entries, photos, book chapters, or anything else.
.. note::
If you used to use Django's older (undocumented) comments framework, you'll
need to upgrade. See the :ref:`upgrade guide <ref-contrib-comments-upgrade>`
for instructions.
Quick start guide
=================
To get started using the ``comments`` app, follow these steps:
#. Install the comments framework by adding ``'django.contrib.comments'`` to
:setting:`INSTALLED_APPS`.
#. Run ``manage.py syncdb`` so that Django will create the comment tables.
#. Add the comment app's URLs to your project's ``urls.py``:
.. code-block:: python
urlpatterns = patterns('',
...
(r'^comments/', include('django.contrib.comments.urls')),
...
)
#. Use the `comment template tags`_ below to embed comments in your
templates.
You might also want to examine the :ref:`ref-contrib-comments-settings`
Comment template tags
=====================
You'll primarily interact with the comment system through a series of template
tags that let you embed comments and generate forms for your users to post them.
Like all custom template tag libraries, you'll need to :ref:`load the custom
tags <loading-custom-template-libraries>` before you can use them::
{% load comments %}
Once loaded you can use the template tags below.
Specifying which object comments are attached to
------------------------------------------------
Django's comments are all "attached" to some parent object. This can be any
instance of a Django model. Each of the tags below gives you a couple of
different ways you can specify which object to attach to:
#. Refer to the object directly -- the more common method. Most of the
time, you'll have some object in the template's context you want
to attach the comment to; you can simply use that object.
For example, in a blog entry page that has a variable named ``entry``,
you could use the following to load the number of comments::
{% get_comment_count for entry as comment_count %}.
#. Refer to the object by content-type and object id. You'd use this method
if you, for some reason, don't actually have direct access to the object.
Following the above example, if you knew the object ID was ``14`` but
didn't have access to the actual object, you could do something like::
{% get_comment_count for blog.entry 14 as comment_count %}
In the above, ``blog.entry`` is the app label and (lower-cased) model
name of the model class.
.. templatetag:: get_comment_list
Displaying comments
-------------------
To get a the list of comments for some object, use :ttag:`get_comment_list`::
{% get_comment_list for [object] as [varname] %}
For example::
{% get_comment_list for event as comment_list %}
{% for comment in comment_list %}
...
{% endfor %}
.. templatetag:: get_comment_count
Counting comments
-----------------
To count comments attached to an object, use :ttag:`get_comment_count`::
{% get_comment_count for [object] as [varname] %}
For example::
{% get_comment_count for event as comment_count %}
<p>This event has {{ comment_count }} comments.</p>
Displaying the comment post form
--------------------------------
To show the form that users will use to post a comment, you can use
:ttag:`render_comment_form` or :ttag:`get_comment_form`
.. templatetag:: render_comment_form
Quickly rendering the comment form
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The easiest way to display a comment form is by using
:ttag:`render_comment_form`::
{% render_comment_form for [object] %}
For example::
{% render_comment_form for event %}
This will render comments using a template named ``comments/form.html``, a
default version of which is included with Django.
.. templatetag:: get_comment_form
Rendering a custom comment form
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want more control over the look and feel of the comment form, you use use
:ttag:`get_comment_form` to get a :ref:`form object <topics-forms-index>` that
you can use in the template::
{% get_comment_form for [object] %}
A complete form might look like::
{% get_comment_form for event %}
<form action="{% comment_form_target %}" method="POST">
{{ form }}
<p class="submit">
<input type="submit" name="submit" class="submit-post" value="Preview">
</p>
</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.
.. templatetag:: comment_form_target
Getting the comment form target
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You may have noticed that the above example uses another template tag --
:ttag:`comment_form_target` -- to actually get the ``action`` attribute of the
form. This will always return the correct URL that comments should be posted to;
you'll always want to use it like above::
<form action="{% comment_form_target %}" method="POST">
Notes on the comment form
-------------------------
The form used by the comment system has a few important anti-spam attributes you
should know about:
* It contains a number of hidden fields that contain timestamps, information
about the object the comment should be attached to, and a "security hash"
used to validate this information. If someone tampers with this data --
something comment spammers will try -- the comment submission will fail.
If you're rendering a custom comment form, you'll need to make sure to
pass these values through unchanged.
* The timestamp is used to ensure that "reply attacks" can't continue very
long. Users who wait too long between requesting the form and posting a
comment will have their submissions refused.
* The comment form includes a "honeypot_" field. It's a trap: if any data is
entered in that field, the comment will be considered spam (spammers often
automatically fill in all fields in an attempt to make valid submissions).
The default form hides this field with a piece of CSS and further labels
it with a warning field; if you use the comment form with a custom
template you should be sure to do the same.
.. _honeypot: http://en.wikipedia.org/wiki/Honeypot_(computing)
More information
================
.. toctree::
:maxdepth: 1
settings
upgrade

View File

@ -0,0 +1,34 @@
.. _ref-contrib-comments-settings:
================
Comment settings
================
These settings configure the behavior of the comments framework:
.. setting:: COMMENTS_HIDE_REMOVED
COMMENTS_HIDE_REMOVED
---------------------
If ``True`` (default), removed comments will be excluded from comment
lists/counts (as taken from template tags). Otherwise, the template author is
responsible for some sort of a "this comment has been removed by the site staff"
message.
.. setting:: COMMENT_MAX_LENGTH
COMMENT_MAX_LENGTH
------------------
The maximum length of the comment field, in characters. Comments longer than
this will be rejected. Defaults to 3000.
.. setting:: COMENTS_APP
COMENTS_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.

View File

@ -0,0 +1,63 @@
.. _ref-contrib-comments-upgrade:
===============================================
Upgrading from Django's previous comment system
===============================================
Prior versions of Django included an outdated, undocumented comment system. Users who reverse-engineered this framework will need to upgrade to use the
new comment system; this guide explains how.
The main changes from the old system are:
* This new system is documented.
* It uses modern Django features like :ref:`forms <topics-forms-index>` and
:ref:`modelforms <topics-forms-modelforms>`.
* It has a single ``Comment`` model instead of separate ``FreeComment`` and
``Comment`` models.
* Comments have "email" and "URL" fields.
* No ratings, photos and karma. This should only effect World Online.
* The ``{% comment_form %}`` tag no longer exists. Instead, there's now two
functions: ``{% get_comment_form %}``, which returns a form for posting a
new comment, and ``{% render_comment_form %}``, which renders said form
using the ``comments/form.html`` template.
Upgrading data
--------------
The data models have changed, as have the table names. To transfer your data into the new system, you'll need to directly run the following SQL:
.. code-block:: sql
BEGIN;
INSERT INTO django_comments
(content_type_id, object_pk, site_id, user_name, user_email, user_url,
comment, submit_date, ip_address, is_public, is_removed)
SELECT
content_type_id, object_id, site_id, person_name, '', '', comment,
submit_date, ip_address, is_public, approved
FROM comments_freecomment;
INSERT INTO django_comments
(content_type_id, object_pk, site_id, user_id, comment, submit_date,
ip_address, is_public, is_removed)
SELECT
content_type_id, object_id, site_id, user_id, comment, submit_date,
ip_address, is_public, is_removed
FROM comments_comment;
UPDATE django_comments SET user_name = (
SELECT username FROM auth_user
WHERE django_comments.user_id = auth_user.id
);
UPDATE django_comments SET user_email = (
SELECT email FROM auth_user
WHERE django_comments.user_id = auth_user.id
);
COMMIT;

View File

@ -26,6 +26,7 @@ those packages have.
admin
auth
comments/index
contenttypes
csrf
databrowse
@ -58,7 +59,9 @@ See :ref:`topics-auth`.
comments
========
A simple yet flexible comments system. This is not yet documented.
**New in Django development version.**
A simple yet flexible comments system. See :ref:`ref-contrib-comments-index`.
contenttypes
============

View File

@ -607,6 +607,8 @@ along with all the fields available on that object.
Taken together, the documentation pages should tell you every tag, filter,
variable and object available to you in a given template.
.. _loading-custom-template-libraries:
Custom tag and filter libraries
===============================

View File

@ -0,0 +1,43 @@
[
{
"model" : "comment_tests.author",
"pk" : 1,
"fields" : {
"first_name" : "John",
"last_name" : "Smith"
}
},
{
"model" : "comment_tests.author",
"pk" : 2,
"fields" : {
"first_name" : "Peter",
"last_name" : "Jones"
}
},
{
"model" : "comment_tests.article",
"pk" : 1,
"fields" : {
"author" : 1,
"headline" : "Man Bites Dog"
}
},
{
"model" : "comment_tests.article",
"pk" : 2,
"fields" : {
"author" : 2,
"headline" : "Dog Bites Man"
}
},
{
"model" : "auth.user",
"pk" : 100,
"fields" : {
"username" : "normaluser",
"password" : "34ea4aaaf24efcbb4b30d27302f8657f"
}
}
]

View File

@ -0,0 +1,22 @@
"""
Comments may be attached to any object. See the comment documentation for
more information.
"""
from django.db import models
from django.test import TestCase
class Author(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
def __str__(self):
return '%s %s' % (self.first_name, self.last_name)
class Article(models.Model):
author = models.ForeignKey(Author)
headline = models.CharField(max_length=100)
def __str__(self):
return self.headline

View File

@ -0,0 +1,90 @@
from django.contrib.auth.models import User
from django.contrib.comments.forms import CommentForm
from django.contrib.comments.models import Comment
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.test import TestCase
from regressiontests.comment_tests.models import Article, Author
# Shortcut
CT = ContentType.objects.get_for_model
# Helper base class for comment tests that need data.
class CommentTestCase(TestCase):
fixtures = ["comment_tests"]
def setUp(self):
settings.ROOT_URLCONF = "django.contrib.comments.urls"
def createSomeComments(self):
# Two anonymous comments on two different objects
c1 = Comment.objects.create(
content_type = CT(Article),
object_pk = "1",
user_name = "Joe Somebody",
user_email = "jsomebody@example.com",
user_url = "http://example.com/~joe/",
comment = "First!",
site = Site.objects.get_current(),
)
c2 = Comment.objects.create(
content_type = CT(Author),
object_pk = "1",
user_name = "Joe Somebody",
user_email = "jsomebody@example.com",
user_url = "http://example.com/~joe/",
comment = "First here, too!",
site = Site.objects.get_current(),
)
# Two authenticated comments: one on the same Article, and
# one on a different Author
user = User.objects.create(
username = "frank_nobody",
first_name = "Frank",
last_name = "Nobody",
email = "fnobody@example.com",
password = "",
is_staff = False,
is_active = True,
is_superuser = False,
)
c3 = Comment.objects.create(
content_type = CT(Article),
object_pk = "1",
user = user,
user_url = "http://example.com/~frank/",
comment = "Damn, I wanted to be first.",
site = Site.objects.get_current(),
)
c4 = Comment.objects.create(
content_type = CT(Author),
object_pk = "2",
user = user,
user_url = "http://example.com/~frank/",
comment = "You get here first, too?",
site = Site.objects.get_current(),
)
return c1, c2, c3, c4
def getData(self):
return {
'name' : 'Jim Bob',
'email' : 'jim.bob@example.com',
'url' : '',
'comment' : 'This is my comment',
}
def getValidData(self, obj):
f = CommentForm(obj)
d = self.getData()
d.update(f.initial)
return d
from regressiontests.comment_tests.tests.app_api_tests import *
from regressiontests.comment_tests.tests.model_tests import *
from regressiontests.comment_tests.tests.comment_form_tests import *
from regressiontests.comment_tests.tests.templatetag_tests import *
from regressiontests.comment_tests.tests.comment_view_tests import *
from regressiontests.comment_tests.tests.moderation_view_tests import *

View File

@ -0,0 +1,30 @@
from django.conf import settings
from django.contrib import comments
from django.contrib.comments.models import Comment
from django.contrib.comments.forms import CommentForm
from regressiontests.comment_tests.tests import CommentTestCase
class CommentAppAPITests(CommentTestCase):
"""Tests for the "comment app" API"""
def testGetCommentApp(self):
self.assertEqual(comments.get_comment_app(), comments)
def testGetForm(self):
self.assertEqual(comments.get_form(), CommentForm)
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,81 @@
import time
from django.conf import settings
from django.contrib.comments.models import Comment
from django.contrib.comments.forms import CommentForm
from regressiontests.comment_tests.models import Article
from regressiontests.comment_tests.tests import CommentTestCase
class CommentFormTests(CommentTestCase):
def testInit(self):
f = CommentForm(Article.objects.get(pk=1))
self.assertEqual(f.initial['content_type'], str(Article._meta))
self.assertEqual(f.initial['object_pk'], "1")
self.failIfEqual(f.initial['security_hash'], None)
self.failIfEqual(f.initial['timestamp'], None)
def testValidPost(self):
a = Article.objects.get(pk=1)
f = CommentForm(a, data=self.getValidData(a))
self.assert_(f.is_valid(), f.errors)
return f
def tamperWithForm(self, **kwargs):
a = Article.objects.get(pk=1)
d = self.getValidData(a)
d.update(kwargs)
f = CommentForm(Article.objects.get(pk=1), data=d)
self.failIf(f.is_valid())
return f
def testHoneypotTampering(self):
self.tamperWithForm(honeypot="I am a robot")
def testTimestampTampering(self):
self.tamperWithForm(timestamp=str(time.time() - 28800))
def testSecurityHashTampering(self):
self.tamperWithForm(security_hash="Nobody expects the Spanish Inquisition!")
def testContentTypeTampering(self):
self.tamperWithForm(content_type="auth.user")
def testObjectPKTampering(self):
self.tamperWithForm(object_pk="3")
def testSecurityErrors(self):
f = self.tamperWithForm(honeypot="I am a robot")
self.assert_("honeypot" in f.security_errors())
def testGetCommentObject(self):
f = self.testValidPost()
c = f.get_comment_object()
self.assert_(isinstance(c, Comment))
self.assertEqual(c.content_object, Article.objects.get(pk=1))
self.assertEqual(c.comment, "This is my comment")
c.save()
self.assertEqual(Comment.objects.count(), 1)
def testProfanities(self):
"""Test COMMENTS_ALLOW_PROFANITIES and PROFANITIES_LIST settings"""
a = Article.objects.get(pk=1)
d = self.getValidData(a)
# Save settings in case other tests need 'em
saved = settings.PROFANITIES_LIST, settings.COMMENTS_ALLOW_PROFANITIES
# Don't wanna swear in the unit tests if we don't have to...
settings.PROFANITIES_LIST = ["rooster"]
# Try with COMMENTS_ALLOW_PROFANITIES off
settings.COMMENTS_ALLOW_PROFANITIES = False
f = CommentForm(a, data=dict(d, comment="What a rooster!"))
self.failIf(f.is_valid())
# Now with COMMENTS_ALLOW_PROFANITIES on
settings.COMMENTS_ALLOW_PROFANITIES = True
f = CommentForm(a, data=dict(d, comment="What a rooster!"))
self.failUnless(f.is_valid())
# Restore settings
settings.PROFANITIES_LIST, settings.COMMENTS_ALLOW_PROFANITIES = saved

View File

@ -0,0 +1,166 @@
from django.conf import settings
from django.contrib.comments import signals
from django.contrib.comments.models import Comment
from regressiontests.comment_tests.models import Article
from regressiontests.comment_tests.tests import CommentTestCase
class CommentViewTests(CommentTestCase):
def testPostCommentHTTPMethods(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
response = self.client.get("/post/", data)
self.assertEqual(response.status_code, 405)
self.assertEqual(response["Allow"], "POST")
def testPostCommentMissingCtype(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
del data["content_type"]
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 400)
def testPostCommentBadCtype(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
data["content_type"] = "Nobody expects the Spanish Inquisition!"
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 400)
def testPostCommentMissingObjectPK(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
del data["object_pk"]
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 400)
def testPostCommentBadObjectPK(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
data["object_pk"] = "14"
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 400)
def testCommentPreview(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
data["submit"] = "preview"
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "comments/preview.html")
def testHashTampering(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
data["security_hash"] = "Nobody expects the Spanish Inquisition!"
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 400)
def testDebugCommentErrors(self):
"""The debug error template should be shown only if DEBUG is True"""
olddebug = settings.DEBUG
settings.DEBUG = True
a = Article.objects.get(pk=1)
data = self.getValidData(a)
data["security_hash"] = "Nobody expects the Spanish Inquisition!"
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 400)
self.assertTemplateUsed(response, "comments/400-debug.html")
settings.DEBUG = False
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 400)
self.assertTemplateNotUsed(response, "comments/400-debug.html")
settings.DEBUG = olddebug
def testCreateValidComment(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
self.response = self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4")
self.assertEqual(self.response.status_code, 302)
self.assertEqual(Comment.objects.count(), 1)
c = Comment.objects.all()[0]
self.assertEqual(c.ip_address, "1.2.3.4")
self.assertEqual(c.comment, "This is my comment")
def testPreventDuplicateComments(self):
"""Prevent posting the exact same comment twice"""
a = Article.objects.get(pk=1)
data = self.getValidData(a)
self.client.post("/post/", data)
self.client.post("/post/", data)
self.assertEqual(Comment.objects.count(), 1)
# This should not trigger the duplicate prevention
self.client.post("/post/", dict(data, comment="My second comment."))
self.assertEqual(Comment.objects.count(), 2)
def testCommentSignals(self):
"""Test signals emitted by the comment posting view"""
# callback
def receive(sender, **kwargs):
self.assertEqual(sender.comment, "This is my comment")
# TODO: Get the two commented tests below to work.
# self.assertEqual(form_data["comment"], "This is my comment")
# self.assertEqual(request.method, "POST")
received_signals.append(kwargs.get('signal'))
# Connect signals and keep track of handled ones
received_signals = []
excepted_signals = [signals.comment_will_be_posted, signals.comment_was_posted]
for signal in excepted_signals:
signal.connect(receive)
# Post a comment and check the signals
self.testCreateValidComment()
self.assertEqual(received_signals, excepted_signals)
def testWillBePostedSignal(self):
"""
Test that the comment_will_be_posted signal can prevent the comment from
actually getting saved
"""
def receive(sender, **kwargs): return False
signals.comment_will_be_posted.connect(receive)
a = Article.objects.get(pk=1)
data = self.getValidData(a)
response = self.client.post("/post/", data)
self.assertEqual(response.status_code, 400)
self.assertEqual(Comment.objects.count(), 0)
def testWillBePostedSignalModifyComment(self):
"""
Test that the comment_will_be_posted signal can modify a comment before
it gets posted
"""
def receive(sender, **kwargs):
sender.is_public = False # a bad but effective spam filter :)...
signals.comment_will_be_posted.connect(receive)
self.testCreateValidComment()
c = Comment.objects.all()[0]
self.failIf(c.is_public)
def testCommentNext(self):
"""Test the different "next" actions the comment view can take"""
a = Article.objects.get(pk=1)
data = self.getValidData(a)
response = self.client.post("/post/", data)
self.assertEqual(response["Location"], "http://testserver/posted/?c=1")
data["next"] = "/somewhere/else/"
data["comment"] = "This is another comment"
response = self.client.post("/post/", data)
self.assertEqual(response["Location"], "http://testserver/somewhere/else/?c=2")
def testCommentDoneView(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
response = self.client.post("/post/", data)
response = self.client.get("/posted/", {'c':1})
self.assertTemplateUsed(response, "comments/posted.html")
self.assertEqual(response.context[0]["comment"], Comment.objects.get(pk=1))

View File

@ -0,0 +1,48 @@
from django.contrib.comments.models import Comment
from regressiontests.comment_tests.models import Author, Article
from regressiontests.comment_tests.tests import CommentTestCase
class CommentModelTests(CommentTestCase):
def testSave(self):
for c in self.createSomeComments():
self.failIfEqual(c.submit_date, None)
def testUserProperties(self):
c1, c2, c3, c4 = self.createSomeComments()
self.assertEqual(c1.name, "Joe Somebody")
self.assertEqual(c2.email, "jsomebody@example.com")
self.assertEqual(c3.name, "Frank Nobody")
self.assertEqual(c3.url, "http://example.com/~frank/")
self.assertEqual(c1.user, None)
self.assertEqual(c3.user, c4.user)
class CommentManagerTests(CommentTestCase):
def testInModeration(self):
"""Comments that aren't public are considered in moderation"""
c1, c2, c3, c4 = self.createSomeComments()
c1.is_public = False
c2.is_public = False
c1.save()
c2.save()
moderated_comments = list(Comment.objects.in_moderation().order_by("id"))
self.assertEqual(moderated_comments, [c1, c2])
def testRemovedCommentsNotInModeration(self):
"""Removed comments are not considered in moderation"""
c1, c2, c3, c4 = self.createSomeComments()
c1.is_public = False
c2.is_public = False
c2.is_removed = True
c1.save()
c2.save()
moderated_comments = list(Comment.objects.in_moderation())
self.assertEqual(moderated_comments, [c1])
def testForModel(self):
c1, c2, c3, c4 = self.createSomeComments()
article_comments = list(Comment.objects.for_model(Article).order_by("id"))
author_comments = list(Comment.objects.for_model(Author.objects.get(pk=1)))
self.assertEqual(article_comments, [c1, c3])
self.assertEqual(author_comments, [c2])

View File

@ -0,0 +1,181 @@
from django.contrib.comments.models import Comment, CommentFlag
from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType
from regressiontests.comment_tests.tests import CommentTestCase
from django.contrib.comments import signals
class FlagViewTests(CommentTestCase):
def testFlagGet(self):
"""GET the flag view: render a confirmation page."""
self.createSomeComments()
self.client.login(username="normaluser", password="normaluser")
response = self.client.get("/flag/1/")
self.assertTemplateUsed(response, "comments/flag.html")
def testFlagPost(self):
"""POST the flag view: actually flag the view (nice for XHR)"""
self.createSomeComments()
self.client.login(username="normaluser", password="normaluser")
response = self.client.post("/flag/1/")
self.assertEqual(response["Location"], "http://testserver/flagged/?c=1")
c = Comment.objects.get(pk=1)
self.assertEqual(c.flags.filter(flag=CommentFlag.SUGGEST_REMOVAL).count(), 1)
return c
def testFlagPostTwice(self):
"""Users don't get to flag comments more than once."""
c = self.testFlagPost()
self.client.post("/flag/1/")
self.client.post("/flag/1/")
self.assertEqual(c.flags.filter(flag=CommentFlag.SUGGEST_REMOVAL).count(), 1)
def testFlagAnon(self):
"""GET/POST the flag view while not logged in: redirect to log in."""
self.createSomeComments()
response = self.client.get("/flag/1/")
self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/flag/1/")
response = self.client.post("/flag/1/")
self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/flag/1/")
def testFlaggedView(self):
self.createSomeComments()
response = self.client.get("/flagged/", data={"c":1})
self.assertTemplateUsed(response, "comments/flagged.html")
def testFlagSignals(self):
"""Test signals emitted by the comment flag view"""
# callback
def receive(sender, **kwargs):
flag = sender.flags.get(id=1)
self.assertEqual(flag.flag, CommentFlag.SUGGEST_REMOVAL)
self.assertEqual(flag.user.username, "normaluser")
received_signals.append(kwargs.get('signal'))
# Connect signals and keep track of handled ones
received_signals = []
signals.comment_was_flagged.connect(receive)
# Post a comment and check the signals
self.testFlagPost()
self.assertEqual(received_signals, [signals.comment_was_flagged])
def makeModerator(username):
u = User.objects.get(username=username)
ct = ContentType.objects.get_for_model(Comment)
p = Permission.objects.get(content_type=ct, codename="can_moderate")
u.user_permissions.add(p)
class DeleteViewTests(CommentTestCase):
def testDeletePermissions(self):
"""The delete view should only be accessible to 'moderators'"""
self.createSomeComments()
self.client.login(username="normaluser", password="normaluser")
response = self.client.get("/delete/1/")
self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/delete/1/")
makeModerator("normaluser")
response = self.client.get("/delete/1/")
self.assertEqual(response.status_code, 200)
def testDeletePost(self):
"""POSTing the delete view should mark the comment as removed"""
self.createSomeComments()
makeModerator("normaluser")
self.client.login(username="normaluser", password="normaluser")
response = self.client.post("/delete/1/")
self.assertEqual(response["Location"], "http://testserver/deleted/?c=1")
c = Comment.objects.get(pk=1)
self.failUnless(c.is_removed)
self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_DELETION, user__username="normaluser").count(), 1)
def testDeleteSignals(self):
def receive(sender, **kwargs):
received_signals.append(kwargs.get('signal'))
# Connect signals and keep track of handled ones
received_signals = []
signals.comment_was_flagged.connect(receive)
# Post a comment and check the signals
self.testDeletePost()
self.assertEqual(received_signals, [signals.comment_was_flagged])
def testDeletedView(self):
self.createSomeComments()
response = self.client.get("/deleted/", data={"c":1})
self.assertTemplateUsed(response, "comments/deleted.html")
class ApproveViewTests(CommentTestCase):
def testApprovePermissions(self):
"""The delete view should only be accessible to 'moderators'"""
self.createSomeComments()
self.client.login(username="normaluser", password="normaluser")
response = self.client.get("/approve/1/")
self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/approve/1/")
makeModerator("normaluser")
response = self.client.get("/approve/1/")
self.assertEqual(response.status_code, 200)
def testApprovePost(self):
"""POSTing the delete view should mark the comment as removed"""
c1, c2, c3, c4 = self.createSomeComments()
c1.is_public = False; c1.save()
makeModerator("normaluser")
self.client.login(username="normaluser", password="normaluser")
response = self.client.post("/approve/1/")
self.assertEqual(response["Location"], "http://testserver/approved/?c=1")
c = Comment.objects.get(pk=1)
self.failUnless(c.is_public)
self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_APPROVAL, user__username="normaluser").count(), 1)
def testApproveSignals(self):
def receive(sender, **kwargs):
received_signals.append(kwargs.get('signal'))
# Connect signals and keep track of handled ones
received_signals = []
signals.comment_was_flagged.connect(receive)
# Post a comment and check the signals
self.testApprovePost()
self.assertEqual(received_signals, [signals.comment_was_flagged])
def testApprovedView(self):
self.createSomeComments()
response = self.client.get("/approved/", data={"c":1})
self.assertTemplateUsed(response, "comments/approved.html")
class ModerationQueueTests(CommentTestCase):
def testModerationQueuePermissions(self):
"""Only moderators can view the moderation queue"""
self.client.login(username="normaluser", password="normaluser")
response = self.client.get("/moderate/")
self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/moderate/")
makeModerator("normaluser")
response = self.client.get("/moderate/")
self.assertEqual(response.status_code, 200)
def testModerationQueueContents(self):
"""Moderation queue should display non-public, non-removed comments."""
c1, c2, c3, c4 = self.createSomeComments()
makeModerator("normaluser")
self.client.login(username="normaluser", password="normaluser")
c1.is_public = c2.is_public = False
c1.save(); c2.save()
response = self.client.get("/moderate/")
self.assertEqual(list(response.context[0]["comments"]), [c1, c2])
c2.is_removed = True
c2.save()
response = self.client.get("/moderate/")
self.assertEqual(list(response.context[0]["comments"]), [c1])

View File

@ -0,0 +1,65 @@
from django.contrib.comments.forms import CommentForm
from django.contrib.comments.models import Comment
from django.template import Template, Context
from regressiontests.comment_tests.models import Article, Author
from regressiontests.comment_tests.tests import CommentTestCase
class CommentTemplateTagTests(CommentTestCase):
def render(self, t, **c):
ctx = Context(c)
out = Template(t).render(ctx)
return ctx, out
def testCommentFormTarget(self):
ctx, out = self.render("{% load comments %}{% comment_form_target %}")
self.assertEqual(out, "/post/")
def testGetCommentForm(self, tag=None):
t = "{% load comments %}" + (tag or "{% get_comment_form for comment_tests.article a.id as form %}")
ctx, out = self.render(t, a=Article.objects.get(pk=1))
self.assertEqual(out, "")
self.assert_(isinstance(ctx["form"], CommentForm))
def testGetCommentFormFromLiteral(self):
self.testGetCommentForm("{% get_comment_form for comment_tests.article 1 as form %}")
def testGetCommentFormFromObject(self):
self.testGetCommentForm("{% get_comment_form for a as form %}")
def testRenderCommentForm(self, tag=None):
t = "{% load comments %}" + (tag or "{% render_comment_form for comment_tests.article a.id %}")
ctx, out = self.render(t, a=Article.objects.get(pk=1))
self.assert_(out.strip().startswith("<form action="))
self.assert_(out.strip().endswith("</form>"))
def testRenderCommentFormFromLiteral(self):
self.testRenderCommentForm("{% render_comment_form for comment_tests.article 1 %}")
def testRenderCommentFormFromObject(self):
self.testRenderCommentForm("{% render_comment_form for a %}")
def testGetCommentCount(self, tag=None):
self.createSomeComments()
t = "{% load comments %}" + (tag or "{% get_comment_count for comment_tests.article a.id as cc %}") + "{{ cc }}"
ctx, out = self.render(t, a=Article.objects.get(pk=1))
self.assertEqual(out, "2")
def testGetCommentCountFromLiteral(self):
self.testGetCommentCount("{% get_comment_count for comment_tests.article 1 as cc %}")
def testGetCommentCountFromObject(self):
self.testGetCommentCount("{% get_comment_count for a as cc %}")
def testGetCommentList(self, tag=None):
c1, c2, c3, c4 = self.createSomeComments()
t = "{% load comments %}" + (tag or "{% get_comment_list for comment_tests.author a.id as cl %}")
ctx, out = self.render(t, a=Author.objects.get(pk=1))
self.assertEqual(out, "")
self.assertEqual(list(ctx["cl"]), [c2])
def testGetCommentListFromLiteral(self):
self.testGetCommentList("{% get_comment_list for comment_tests.author 1 as cl %}")
def testGetCommentListFromObject(self):
self.testGetCommentList("{% get_comment_list for a as cl %}")