Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactored Django's comment system.

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

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


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8557 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit cba91997a24f3cb154c7c51029c6dd91471f8800 1 parent b46e736
Jacob Kaplan-Moss jacobian authored
Showing with 2,409 additions and 1,147 deletions.
  1. +1 −0  AUTHORS
  2. +70 −0 django/contrib/comments/__init__.py
  3. +18 −24 django/contrib/comments/admin.py
  4. +13 −20 django/contrib/comments/feeds.py
  5. +159 −0 django/contrib/comments/forms.py
  6. +22 −0 django/contrib/comments/managers.py
  7. +155 −256 django/contrib/comments/models.py
  8. +21 −0 django/contrib/comments/signals.py
  9. +53 −0 django/contrib/comments/templates/comments/400-debug.html
  10. +14 −0 django/contrib/comments/templates/comments/approve.html
  11. +7 −0 django/contrib/comments/templates/comments/approved.html
  12. +10 −0 django/contrib/comments/templates/comments/base.html
  13. +14 −0 django/contrib/comments/templates/comments/delete.html
  14. +7 −0 django/contrib/comments/templates/comments/deleted.html
  15. +14 −0 django/contrib/comments/templates/comments/flag.html
  16. +7 −0 django/contrib/comments/templates/comments/flagged.html
  17. +19 −38 django/contrib/comments/templates/comments/form.html
  18. +0 −13 django/contrib/comments/templates/comments/freeform.html
  19. +75 −0 django/contrib/comments/templates/comments/moderation_queue.html
  20. +7 −0 django/contrib/comments/templates/comments/posted.html
  21. +34 −0 django/contrib/comments/templates/comments/preview.html
  22. +19 −0 django/contrib/comments/templates/comments/reply.html
  23. +34 −0 django/contrib/comments/templates/comments/reply_preview.html
  24. +201 −282 django/contrib/comments/templatetags/comments.py
  25. +0 −13 django/contrib/comments/tests.py
  26. +15 −0 django/contrib/comments/urls.py
  27. +0 −12 django/contrib/comments/urls/comments.py
  28. +99 −376 django/contrib/comments/views/comments.py
  29. +0 −32 django/contrib/comments/views/karma.py
  30. +186 −0 django/contrib/comments/views/moderation.py
  31. +0 −62 django/contrib/comments/views/userflags.py
  32. +58 −0 django/contrib/comments/views/utils.py
  33. +1 −1  docs/_static/djangodocs.css
  34. +35 −17 docs/index.txt
  35. +212 −0 docs/ref/contrib/comments/index.txt
  36. +34 −0 docs/ref/contrib/comments/settings.txt
  37. +63 −0 docs/ref/contrib/comments/upgrade.txt
  38. +4 −1 docs/ref/contrib/index.txt
  39. +2 −0  docs/topics/templates.txt
  40. 0  {django/contrib/comments/urls → tests/regressiontests/comment_tests}/__init__.py
  41. +43 −0 tests/regressiontests/comment_tests/fixtures/comment_tests.json
  42. +22 −0 tests/regressiontests/comment_tests/models.py
  43. +90 −0 tests/regressiontests/comment_tests/tests/__init__.py
  44. +30 −0 tests/regressiontests/comment_tests/tests/app_api_tests.py
  45. +81 −0 tests/regressiontests/comment_tests/tests/comment_form_tests.py
  46. +166 −0 tests/regressiontests/comment_tests/tests/comment_view_tests.py
  47. +48 −0 tests/regressiontests/comment_tests/tests/model_tests.py
  48. +181 −0 tests/regressiontests/comment_tests/tests/moderation_view_tests.py
  49. +65 −0 tests/regressiontests/comment_tests/tests/templatetag_tests.py
1  AUTHORS
View
@@ -322,6 +322,7 @@ answer newbie questions, and generally made Django that much better:
polpak@yahoo.com
Matthias Pronk <django@masida.nl>
Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
+ Thejaswi Puthraya <thejaswi.puthraya@gmail.com>
Johann Queuniet <johann.queuniet@adh.naellia.eu>
Jan Rademaker
Michael Radziej <mir@noris.de>
70 django/contrib/comments/__init__.py
View
@@ -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,))
42 django/contrib/comments/admin.py
View
@@ -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)
33 django/contrib/comments/feeds.py
View
@@ -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 @@ def description(self):
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
159 django/contrib/comments/forms.py
View
@@ -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()
22 django/contrib/comments/managers.py
View
@@ -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
411 django/contrib/comments/models.py
View
@@ -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
-
-# Option codes for comment-form hidden fields.
-PHOTOS_REQUIRED = 'pr'
-PHOTOS_OPTIONAL = 'pa'
-RATINGS_REQUIRED = 'rr'
-RATINGS_OPTIONAL = 'ra'
-IS_PUBLIC = 'ip'
-
-# What users get if they don't have any karma.
-DEFAULT_KARMA = 5
-KARMA_NEEDED_BEFORE_DISPLAYED = 3
-
-
-class CommentManager(models.Manager):
- def get_security_hash(self, options, photo_options, rating_options, target):
- """
- 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.
- """
- from django.utils.hashcompat import md5_constructor
- return md5_constructor(options + photo_options + rating_options + target + settings.SECRET_KEY).hexdigest()
-
- 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
-
- 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)
+COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000)
- 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
+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")
-
-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)
- objects = CommentManager()
+ # Metadata about the comment
+ site = models.ForeignKey(Site)
class Meta:
- verbose_name = _('comment')
- verbose_name_plural = _('comments')
- ordering = ('-submit_date',)
-
- def __unicode__(self):
- return "%s: %s..." % (self.user.username, self.comment[:100])
+ abstract = True
- def get_absolute_url(self):
- try:
- return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
- except AttributeError:
- return ""
-
- 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_content_object_url(self):
"""
- Returns the object that this comment is a comment on. Returns None if
- the object no longer exists.
+ Get a URL suitable for redirecting to the content object. Uses the
+ ``django.views.defaults.shortcut`` view, which thus must be installed.
"""
- 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')
-
- 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_good_karma_total(self):
- if not hasattr(self, "_karma_total_good"):
- self._fill_karma_cache()
- return self._karma_total_good
-
- def get_bad_karma_total(self):
- if not hasattr(self, "_karma_total_bad"):
- self._fill_karma_cache()
- return self._karma_total_bad
-
- 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_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()}
-
-
-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)
+ return urlresolvers.reverse(
+ "django.views.defaults.shortcut",
+ args=(self.content_type_id, self.object_pk)
+ )
+
+class Comment(BaseCommentAbstractModel):
+ """
+ A user comment about some object.
+ """
+
+ # 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)
+
+ 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.'))
+
+ # Manager
+ objects = CommentManager()
class Meta:
- verbose_name = _('free comment')
- verbose_name_plural = _('free comments')
- ordering = ('-submit_date',)
+ db_table = "django_comments"
+ ordering = ('submit_date',)
+ permissions = [("can_moderate", "Can moderate comments")]
def __unicode__(self):
- return "%s: %s..." % (self.person_name, 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_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.
- """
- 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()
+ Get a dictionary that pulls together information about the poster
+ safely for both authenticated and non-authenticated comments.
- def get_pretty_score(self, score):
+ This dict will have ``name``, ``email``, and ``url`` fields.
"""
- 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()
+ 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
+
+ # 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 _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_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_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_absolute_url(self, anchor_pattern="#c%(id)s"):
+ return self.get_content_object_url() + (anchor_pattern % self.__dict__)
- 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):
+ def get_as_text(self):
"""
- 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.
+ Return this comment as plain text. Useful for emails.
"""
- 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()
+ 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:
+
+ * 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 = _('user flag')
- verbose_name_plural = _('user flags')
- unique_together = (('user', 'comment'),)
+ db_table = 'django_comment_flags'
+ unique_together = [('user', 'comment', 'flag')]
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)
+ return "%s flag of comment ID %s by %s" % \
+ (self.flag, self.comment_id, self.user.username)
- 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()
21 django/contrib/comments/signals.py
View
@@ -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()
53 django/contrib/comments/templates/comments/400-debug.html
View
@@ -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>
14 django/contrib/comments/templates/comments/approve.html
View
@@ -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 %}
7 django/contrib/comments/templates/comments/approved.html
View
@@ -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 %}
10 django/contrib/comments/templates/comments/base.html
View
@@ -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>
14 django/contrib/comments/templates/comments/delete.html
View
@@ -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 %}
7 django/contrib/comments/templates/comments/deleted.html
View
@@ -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 %}
14 django/contrib/comments/templates/comments/flag.html
View
@@ -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 %}
7 django/contrib/comments/templates/comments/flagged.html
View
@@ -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 %}
57 django/contrib/comments/templates/comments/form.html
View
@@ -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>
13 django/contrib/comments/templates/comments/freeform.html
View
@@ -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 %}
75 django/contrib/comments/templates/comments/moderation_queue.html
View
@@ -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 %}
7 django/contrib/comments/templates/comments/posted.html
View
@@ -0,0 +1,7 @@
+{% extends "comments/base.html" %}
+
+{% block title %}Thanks for commenting.{% endblock %}
+
+{% block content %}
+ <h1>Thank you for your comment.</h1>
+{% endblock %}
34 django/contrib/comments/templates/comments/preview.html
View
@@ -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 %}
19 django/contrib/comments/templates/comments/reply.html
View
@@ -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>
34 django/contrib/comments/templates/comments/reply_preview.html
View
@@ -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 %}
483 django/contrib/comments/templatetags/comments.py
View
@@ -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 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
+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.
+ """
+
+ #@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:
+ qs = self.get_query_set(context)
+ context[self.as_varname] = self.get_context_value_from_queryset(context, qs)
+ return ''
+
+ 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:
- self.obj_id = self.obj_id_lookup_var.resolve(context)
+ obj = self.object_expr.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
+ return None, None
+ return ContentType.objects.get_for_model(obj), obj.pk
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)
+ 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:
- 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
+ return None
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
+ context[self.as_varname] = self.get_form(context)
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 {}
+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):
- 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
- return ''
+ 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.
-class DoCommentForm:
+#@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) %}
+ {% get_comment_form for [object] as [varname] %}
+ {% get_comment_form for [app].[model] [object_id] as [varname] %}
+ """
+ return CommentFormNode.handle_token(parser, token)
- Example usage::
+#@register.tag
+def render_comment_form(parser, token):
+ """
+ Render the comment form (as returned by ``{% render_comment_form %}``) through
+ the ``comments/form.html`` template.
- {% get_comment_list for lcom.eventtimes event.id as comment_list %}
+ Syntax::
- Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this::
+ {% render_comment_form for [object] %}
+ {% render_comment_form for [app].[model] [object_id] %}
+ """
+ return RenderCommentFormNode.handle_token(parser, token)
- {% get_comment_list for lcom.eventtimes 23 as comment_list %}
+#@register.simple_tag
+def comment_form_target():
+ """
+ Get the target URL for the comment form.
- To get a list of comments in reverse order -- that is, most recent first --
- pass ``reversed`` as the last param::
+ Example::
- {% get_comment_list for lcom.eventtimes event.id as comment_list reversed %}
+ <form action="{% comment_form_target %}" method="POST">
"""
- def __init__(self, free):
- self.free = free
+ return comments.get_form_target()
- 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)
-
-# 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))
+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)
13 django/contrib/comments/tests.py
View
@@ -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: â...
-"""
-
15 django/contrib/comments/urls.py
View
@@ -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'),
+)
+
12 django/contrib/comments/urls/comments.py
View
@@ -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'),
-)
475 django/contrib/comments/views/comments.py
View
@@ -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
-
-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.
+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
-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),
- ]
- 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]),
- )
-
- def hasNoProfanities(self, field_data, all_data):
- if settings.COMMENTS_ALLOW_PROFANITIES:
- return
- return validators.hasNoProfanities(field_data, all_data)
+ def __init__(self, why):
+ super(CommentPostBadRequest, self).__init__()
+ if settings.DEBUG:
+ self.content = render_to_string("comments/400-debug.html", {"why": why})
- 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)
-
- 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
-
-def post_comment(request, extra_context=None, context_processors=None):
+def post_comment(request, next=None):
"""
- Post a comment
+ Post a comment.
- Redirects to the `comments.comments.comment_was_posted` view upon success.
-
- 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
+ HTTP POST is required. If ``POST['submit'] == "preview"`` or if there are
+ errors a preview template, ``comments/preview.html``, will be rendered.
"""
- 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'
+
+ # 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:
- obj = ContentType.objects.get(pk=content_type_id).get_object_for_this_type(pk=object_id)
+ 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:
- 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'")
+ 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 \