Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit cba9199

Browse files
committed
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
1 parent b46e736 commit cba9199

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2409
-1147
lines changed

‎AUTHORS‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ answer newbie questions, and generally made Django that much better:
322322
polpak@yahoo.com
323323
Matthias Pronk <django@masida.nl>
324324
Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
325+
Thejaswi Puthraya <thejaswi.puthraya@gmail.com>
325326
Johann Queuniet <johann.queuniet@adh.naellia.eu>
326327
Jan Rademaker
327328
Michael Radziej <mir@noris.de>

‎django/contrib/comments/__init__.py‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from django.conf import settings
2+
from django.core import urlresolvers
3+
from django.core.exceptions import ImproperlyConfigured
4+
5+
# Attributes required in the top-level app for COMMENTS_APP
6+
REQUIRED_COMMENTS_APP_ATTRIBUTES = ["get_model", "get_form", "get_form_target"]
7+
8+
def get_comment_app():
9+
"""
10+
Get the comment app (i.e. "django.contrib.comments") as defined in the settings
11+
"""
12+
# Make sure the app's in INSTALLED_APPS
13+
comments_app = getattr(settings, 'COMMENTS_APP', 'django.contrib.comments')
14+
if comments_app not in settings.INSTALLED_APPS:
15+
raise ImproperlyConfigured("The COMMENTS_APP (%r) "\
16+
"must be in INSTALLED_APPS" % settings.COMMENTS_APP)
17+
18+
# Try to import the package
19+
try:
20+
package = __import__(settings.COMMENTS_APP, '', '', [''])
21+
except ImportError:
22+
raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\
23+
"a non-existing package.")
24+
25+
# Make sure some specific attributes exist inside that package.
26+
for attribute in REQUIRED_COMMENTS_APP_ATTRIBUTES:
27+
if not hasattr(package, attribute):
28+
raise ImproperlyConfigured("The COMMENTS_APP package %r does not "\
29+
"define the (required) %r function" % \
30+
(package, attribute))
31+
32+
return package
33+
34+
def get_model():
35+
from django.contrib.comments.models import Comment
36+
return Comment
37+
38+
def get_form():
39+
from django.contrib.comments.forms import CommentForm
40+
return CommentForm
41+
42+
def get_form_target():
43+
return urlresolvers.reverse("django.contrib.comments.views.comments.post_comment")
44+
45+
def get_flag_url(comment):
46+
"""
47+
Get the URL for the "flag this comment" view.
48+
"""
49+
if settings.COMMENTS_APP != __name__ and hasattr(get_comment_app(), "get_flag_url"):
50+
return get_comment_app().get_flag_url(comment)
51+
else:
52+
return urlresolvers.reverse("django.contrib.comments.views.moderation.flag", args=(comment.id,))
53+
54+
def get_delete_url(comment):
55+
"""
56+
Get the URL for the "delete this comment" view.
57+
"""
58+
if settings.COMMENTS_APP != __name__ and hasattr(get_comment_app(), "get_delete_url"):
59+
return get_comment_app().get_flag_url(get_delete_url)
60+
else:
61+
return urlresolvers.reverse("django.contrib.comments.views.moderation.delete", args=(comment.id,))
62+
63+
def get_approve_url(comment):
64+
"""
65+
Get the URL for the "approve this comment from moderation" view.
66+
"""
67+
if settings.COMMENTS_APP != __name__ and hasattr(get_comment_app(), "get_approve_url"):
68+
return get_comment_app().get_approve_url(comment)
69+
else:
70+
return urlresolvers.reverse("django.contrib.comments.views.moderation.approve", args=(comment.id,))

‎django/contrib/comments/admin.py‎

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
11
from django.contrib import admin
2-
from django.contrib.comments.models import Comment, FreeComment
2+
from django.conf import settings
3+
from django.contrib.comments.models import Comment
4+
from django.utils.translation import ugettext_lazy as _
35

4-
5-
class CommentAdmin(admin.ModelAdmin):
6+
class CommentsAdmin(admin.ModelAdmin):
67
fieldsets = (
7-
(None, {'fields': ('content_type', 'object_id', 'site')}),
8-
('Content', {'fields': ('user', 'headline', 'comment')}),
9-
('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}),
10-
('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}),
11-
)
12-
list_display= ('user', 'submit_date', 'content_type', 'get_content_object')
13-
list_filter= ('submit_date',)
14-
date_hierarchy='submit_date'
15-
search_fields= ('comment', 'user__username')
16-
raw_id_fields= ('user',)
8+
(None,
9+
{'fields': ('content_type', 'object_pk', 'site')}
10+
),
11+
(_('Content'),
12+
{'fields': ('user', 'user_name', 'user_email', 'user_url', 'comment')}
13+
),
14+
(_('Metadata'),
15+
{'fields': ('submit_date', 'ip_address', 'is_public', 'is_removed')}
16+
),
17+
)
1718

18-
class FreeCommentAdmin(admin.ModelAdmin):
19-
fieldsets = (
20-
(None, {'fields': ('content_type', 'object_id', 'site')}),
21-
('Content', {'fields': ('person_name', 'comment')}),
22-
('Meta', {'fields': ('is_public', 'ip_address', 'approved')}),
23-
)
24-
list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object')
25-
list_filter = ('submit_date',)
19+
list_display = ('name', 'content_type', 'object_pk', 'ip_address', 'is_public', 'is_removed')
20+
list_filter = ('submit_date', 'site', 'is_public', 'is_removed')
2621
date_hierarchy = 'submit_date'
27-
search_fields = ('comment', 'person_name')
22+
search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')
2823

29-
admin.site.register(Comment, CommentAdmin)
30-
admin.site.register(FreeComment, FreeCommentAdmin)
24+
admin.site.register(Comment, CommentsAdmin)

‎django/contrib/comments/feeds.py‎

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from django.conf import settings
2-
from django.contrib.comments.models import Comment, FreeComment
32
from django.contrib.syndication.feeds import Feed
43
from django.contrib.sites.models import Site
4+
from django.contrib import comments
55

6-
class LatestFreeCommentsFeed(Feed):
7-
"""Feed of latest free comments on the current site."""
8-
9-
comments_class = FreeComment
6+
class LatestCommentFeed(Feed):
7+
"""Feed of latest comments on the current site."""
108

119
def title(self):
1210
if not hasattr(self, '_site'):
@@ -23,22 +21,17 @@ def description(self):
2321
self._site = Site.objects.get_current()
2422
return u"Latest comments on %s" % self._site.name
2523

26-
def get_query_set(self):
27-
return self.comments_class.objects.filter(site__pk=settings.SITE_ID, is_public=True)
28-
2924
def items(self):
30-
return self.get_query_set()[:40]
31-
32-
class LatestCommentsFeed(LatestFreeCommentsFeed):
33-
"""Feed of latest comments on the current site."""
34-
35-
comments_class = Comment
36-
37-
def get_query_set(self):
38-
qs = super(LatestCommentsFeed, self).get_query_set()
39-
qs = qs.filter(is_removed=False)
40-
if settings.COMMENTS_BANNED_USERS_GROUP:
25+
qs = comments.get_model().objects.filter(
26+
site__pk = settings.SITE_ID,
27+
is_public = True,
28+
is_removed = False,
29+
)
30+
if getattr(settings, 'COMMENTS_BANNED_USERS_GROUP', None):
4131
where = ['user_id NOT IN (SELECT user_id FROM auth_users_group WHERE group_id = %s)']
4232
params = [settings.COMMENTS_BANNED_USERS_GROUP]
4333
qs = qs.extra(where=where, params=params)
44-
return qs
34+
return qs[:40]
35+
36+
def item_pubdate(self, item):
37+
return item.submit_date

‎django/contrib/comments/forms.py‎

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import re
2+
import time
3+
import datetime
4+
from sha import sha
5+
from django import forms
6+
from django.forms.util import ErrorDict
7+
from django.conf import settings
8+
from django.http import Http404
9+
from django.contrib.contenttypes.models import ContentType
10+
from models import Comment
11+
from django.utils.text import get_text_list
12+
from django.utils.translation import ngettext
13+
from django.utils.translation import ugettext_lazy as _
14+
15+
COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000)
16+
17+
class CommentForm(forms.Form):
18+
name = forms.CharField(label=_("Name"), max_length=50)
19+
email = forms.EmailField(label=_("Email address"))
20+
url = forms.URLField(label=_("URL"), required=False)
21+
comment = forms.CharField(label=_('Comment'), widget=forms.Textarea,
22+
max_length=COMMENT_MAX_LENGTH)
23+
honeypot = forms.CharField(required=False,
24+
label=_('If you enter anything in this field '\
25+
'your comment will be treated as spam'))
26+
content_type = forms.CharField(widget=forms.HiddenInput)
27+
object_pk = forms.CharField(widget=forms.HiddenInput)
28+
timestamp = forms.IntegerField(widget=forms.HiddenInput)
29+
security_hash = forms.CharField(min_length=40, max_length=40, widget=forms.HiddenInput)
30+
31+
def __init__(self, target_object, data=None, initial=None):
32+
self.target_object = target_object
33+
if initial is None:
34+
initial = {}
35+
initial.update(self.generate_security_data())
36+
super(CommentForm, self).__init__(data=data, initial=initial)
37+
38+
def get_comment_object(self):
39+
"""
40+
Return a new (unsaved) comment object based on the information in this
41+
form. Assumes that the form is already validated and will throw a
42+
ValueError if not.
43+
44+
Does not set any of the fields that would come from a Request object
45+
(i.e. ``user`` or ``ip_address``).
46+
"""
47+
if not self.is_valid():
48+
raise ValueError("get_comment_object may only be called on valid forms")
49+
50+
new = Comment(
51+
content_type = ContentType.objects.get_for_model(self.target_object),
52+
object_pk = str(self.target_object._get_pk_val()),
53+
user_name = self.cleaned_data["name"],
54+
user_email = self.cleaned_data["email"],
55+
user_url = self.cleaned_data["url"],
56+
comment = self.cleaned_data["comment"],
57+
submit_date = datetime.datetime.now(),
58+
site_id = settings.SITE_ID,
59+
is_public = True,
60+
is_removed = False,
61+
)
62+
63+
# Check that this comment isn't duplicate. (Sometimes people post comments
64+
# twice by mistake.) If it is, fail silently by returning the old comment.
65+
possible_duplicates = Comment.objects.filter(
66+
content_type = new.content_type,
67+
object_pk = new.object_pk,
68+
user_name = new.user_name,
69+
user_email = new.user_email,
70+
user_url = new.user_url,
71+
)
72+
for old in possible_duplicates:
73+
if old.submit_date.date() == new.submit_date.date() and old.comment == new.comment:
74+
return old
75+
76+
return new
77+
78+
def security_errors(self):
79+
"""Return just those errors associated with security"""
80+
errors = ErrorDict()
81+
for f in ["honeypot", "timestamp", "security_hash"]:
82+
if f in self.errors:
83+
errors[f] = self.errors[f]
84+
return errors
85+
86+
def clean_honeypot(self):
87+
"""Check that nothing's been entered into the honeypot."""
88+
value = self.cleaned_data["honeypot"]
89+
if value:
90+
raise forms.ValidationError(self.fields["honeypot"].label)
91+
return value
92+
93+
def clean_security_hash(self):
94+
"""Check the security hash."""
95+
security_hash_dict = {
96+
'content_type' : self.data.get("content_type", ""),
97+
'object_pk' : self.data.get("object_pk", ""),
98+
'timestamp' : self.data.get("timestamp", ""),
99+
}
100+
expected_hash = self.generate_security_hash(**security_hash_dict)
101+
actual_hash = self.cleaned_data["security_hash"]
102+
if expected_hash != actual_hash:
103+
raise forms.ValidationError("Security hash check failed.")
104+
return actual_hash
105+
106+
def clean_timestamp(self):
107+
"""Make sure the timestamp isn't too far (> 2 hours) in the past."""
108+
ts = self.cleaned_data["timestamp"]
109+
if time.time() - ts > (2 * 60 * 60):
110+
raise forms.ValidationError("Timestamp check failed")
111+
return ts
112+
113+
def clean_comment(self):
114+
"""
115+
If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't
116+
contain anything in PROFANITIES_LIST.
117+
"""
118+
comment = self.cleaned_data["comment"]
119+
if settings.COMMENTS_ALLOW_PROFANITIES == False:
120+
# Logic adapted from django.core.validators; it's not clear if they
121+
# should be used in newforms or will be deprecated along with the
122+
# rest of oldforms
123+
bad_words = [w for w in settings.PROFANITIES_LIST if w in comment.lower()]
124+
if bad_words:
125+
plural = len(bad_words) > 1
126+
raise forms.ValidationError(ngettext(
127+
"Watch your mouth! The word %s is not allowed here.",
128+
"Watch your mouth! The words %s are not allowed here.", plural) % \
129+
get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in bad_words], 'and'))
130+
return comment
131+
132+
def generate_security_data(self):
133+
"""Generate a dict of security data for "initial" data."""
134+
timestamp = int(time.time())
135+
security_dict = {
136+
'content_type' : str(self.target_object._meta),
137+
'object_pk' : str(self.target_object._get_pk_val()),
138+
'timestamp' : str(timestamp),
139+
'security_hash' : self.initial_security_hash(timestamp),
140+
}
141+
return security_dict
142+
143+
def initial_security_hash(self, timestamp):
144+
"""
145+
Generate the initial security hash from self.content_object
146+
and a (unix) timestamp.
147+
"""
148+
149+
initial_security_dict = {
150+
'content_type' : str(self.target_object._meta),
151+
'object_pk' : str(self.target_object._get_pk_val()),
152+
'timestamp' : str(timestamp),
153+
}
154+
return self.generate_security_hash(**initial_security_dict)
155+
156+
def generate_security_hash(self, content_type, object_pk, timestamp):
157+
"""Generate a (SHA1) security hash from the provided info."""
158+
info = (content_type, object_pk, timestamp, settings.SECRET_KEY)
159+
return sha("".join(info)).hexdigest()

‎django/contrib/comments/managers.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.db import models
2+
from django.dispatch import dispatcher
3+
from django.contrib.contenttypes.models import ContentType
4+
5+
class CommentManager(models.Manager):
6+
7+
def in_moderation(self):
8+
"""
9+
QuerySet for all comments currently in the moderation queue.
10+
"""
11+
return self.get_query_set().filter(is_public=False, is_removed=False)
12+
13+
def for_model(self, model):
14+
"""
15+
QuerySet for all comments for a particular model (either an instance or
16+
a class).
17+
"""
18+
ct = ContentType.objects.get_for_model(model)
19+
qs = self.get_query_set().filter(content_type=ct)
20+
if isinstance(model, models.Model):
21+
qs = qs.filter(object_pk=model._get_pk_val())
22+
return qs

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /