Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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 authored August 25, 2008

Showing 49 changed files with 2,409 additions and 1,147 deletions. Show diff stats Hide diff stats

  1. 1  AUTHORS
  2. 70  django/contrib/comments/__init__.py
  3. 42  django/contrib/comments/admin.py
  4. 33  django/contrib/comments/feeds.py
  5. 159  django/contrib/comments/forms.py
  6. 22  django/contrib/comments/managers.py
  7. 411  django/contrib/comments/models.py
  8. 21  django/contrib/comments/signals.py
  9. 53  django/contrib/comments/templates/comments/400-debug.html
  10. 14  django/contrib/comments/templates/comments/approve.html
  11. 7  django/contrib/comments/templates/comments/approved.html
  12. 10  django/contrib/comments/templates/comments/base.html
  13. 14  django/contrib/comments/templates/comments/delete.html
  14. 7  django/contrib/comments/templates/comments/deleted.html
  15. 14  django/contrib/comments/templates/comments/flag.html
  16. 7  django/contrib/comments/templates/comments/flagged.html
  17. 57  django/contrib/comments/templates/comments/form.html
  18. 13  django/contrib/comments/templates/comments/freeform.html
  19. 75  django/contrib/comments/templates/comments/moderation_queue.html
  20. 7  django/contrib/comments/templates/comments/posted.html
  21. 34  django/contrib/comments/templates/comments/preview.html
  22. 19  django/contrib/comments/templates/comments/reply.html
  23. 34  django/contrib/comments/templates/comments/reply_preview.html
  24. 483  django/contrib/comments/templatetags/comments.py
  25. 13  django/contrib/comments/tests.py
  26. 15  django/contrib/comments/urls.py
  27. 12  django/contrib/comments/urls/comments.py
  28. 475  django/contrib/comments/views/comments.py
  29. 32  django/contrib/comments/views/karma.py
  30. 186  django/contrib/comments/views/moderation.py
  31. 62  django/contrib/comments/views/userflags.py
  32. 58  django/contrib/comments/views/utils.py
  33. 2  docs/_static/djangodocs.css
  34. 52  docs/index.txt
  35. 212  docs/ref/contrib/comments/index.txt
  36. 34  docs/ref/contrib/comments/settings.txt
  37. 63  docs/ref/contrib/comments/upgrade.txt
  38. 5  docs/ref/contrib/index.txt
  39. 2  docs/topics/templates.txt
  40. 0  {django/contrib/comments/urls → tests/regressiontests/comment_tests}/__init__.py
  41. 43  tests/regressiontests/comment_tests/fixtures/comment_tests.json
  42. 22  tests/regressiontests/comment_tests/models.py
  43. 90  tests/regressiontests/comment_tests/tests/__init__.py
  44. 30  tests/regressiontests/comment_tests/tests/app_api_tests.py
  45. 81  tests/regressiontests/comment_tests/tests/comment_form_tests.py
  46. 166  tests/regressiontests/comment_tests/tests/comment_view_tests.py
  47. 48  tests/regressiontests/comment_tests/tests/model_tests.py
  48. 181  tests/regressiontests/comment_tests/tests/moderation_view_tests.py
  49. 65  tests/regressiontests/comment_tests/tests/templatetag_tests.py
1  AUTHORS
@@ -322,6 +322,7 @@ answer newbie questions, and generally made Django that much better:
322 322
     polpak@yahoo.com
323 323
     Matthias Pronk <django@masida.nl>
324 324
     Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
  325
+    Thejaswi Puthraya <thejaswi.puthraya@gmail.com>
325 326
     Johann Queuniet <johann.queuniet@adh.naellia.eu>
326 327
     Jan Rademaker
327 328
     Michael Radziej <mir@noris.de>
70  django/contrib/comments/__init__.py
... ...
@@ -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,))
42  django/contrib/comments/admin.py
... ...
@@ -1,30 +1,24 @@
1 1
 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 _
3 5
 
4  
-
5  
-class CommentAdmin(admin.ModelAdmin):
  6
+class CommentsAdmin(admin.ModelAdmin):
6 7
     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
+     )
17 18
 
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')
26 21
     date_hierarchy = 'submit_date'
27  
-    search_fields = ('comment', 'person_name')
  22
+    search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')
28 23
 
29  
-admin.site.register(Comment, CommentAdmin)
30  
-admin.site.register(FreeComment, FreeCommentAdmin)
  24
+admin.site.register(Comment, CommentsAdmin)
33  django/contrib/comments/feeds.py
... ...
@@ -1,12 +1,10 @@
1 1
 from django.conf import settings
2  
-from django.contrib.comments.models import Comment, FreeComment
3 2
 from django.contrib.syndication.feeds import Feed
4 3
 from django.contrib.sites.models import Site
  4
+from django.contrib import comments
5 5
 
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."""
10 8
 
11 9
     def title(self):
12 10
         if not hasattr(self, '_site'):
@@ -23,22 +21,17 @@ def description(self):
23 21
             self._site = Site.objects.get_current()
24 22
         return u"Latest comments on %s" % self._site.name
25 23
 
26  
-    def get_query_set(self):
27  
-        return self.comments_class.objects.filter(site__pk=settings.SITE_ID, is_public=True)
28  
-
29 24
     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):
41 31
             where = ['user_id NOT IN (SELECT user_id FROM auth_users_group WHERE group_id = %s)']
42 32
             params = [settings.COMMENTS_BANNED_USERS_GROUP]
43 33
             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
159  django/contrib/comments/forms.py
... ...
@@ -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()
22  django/contrib/comments/managers.py
... ...
@@ -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
411  django/contrib/comments/models.py
... ...
@@ -1,286 +1,185 @@
1 1
 import datetime
2  
-
3  
-from django.db import models
  2
+from django.contrib.auth.models import User
  3
+from django.contrib.comments.managers import CommentManager
  4
+from django.contrib.contenttypes import generic
4 5
 from django.contrib.contenttypes.models import ContentType
5 6
 from django.contrib.sites.models import Site
6  
-from django.contrib.auth.models import User
  7
+from django.db import models
  8
+from django.core import urlresolvers, validators
7 9
 from django.utils.translation import ugettext_lazy as _
8 10
 from django.conf import settings
9 11
 
10  
-MIN_PHOTO_DIMENSION = 5
11  
-MAX_PHOTO_DIMENSION = 1000
12  
-
13  
-# Option codes for comment-form hidden fields.
14  
-PHOTOS_REQUIRED = 'pr'
15  
-PHOTOS_OPTIONAL = 'pa'
16  
-RATINGS_REQUIRED = 'rr'
17  
-RATINGS_OPTIONAL = 'ra'
18  
-IS_PUBLIC = 'ip'
19  
-
20  
-# What users get if they don't have any karma.
21  
-DEFAULT_KARMA = 5
22  
-KARMA_NEEDED_BEFORE_DISPLAYED = 3
23  
-
24  
-
25  
-class CommentManager(models.Manager):
26  
-    def get_security_hash(self, options, photo_options, rating_options, target):
27  
-        """
28  
-        Returns the MD5 hash of the given options (a comma-separated string such as
29  
-        'pa,ra') and target (something like 'lcom.eventtimes:5157'). Used to
30  
-        validate that submitted form options have not been tampered-with.
31  
-        """
32  
-        from django.utils.hashcompat import md5_constructor
33  
-        return md5_constructor(options + photo_options + rating_options + target + settings.SECRET_KEY).hexdigest()
34  
-
35  
-    def get_rating_options(self, rating_string):
36  
-        """
37  
-        Given a rating_string, this returns a tuple of (rating_range, options).
38  
-        >>> s = "scale:1-10|First_category|Second_category"
39  
-        >>> Comment.objects.get_rating_options(s)
40  
-        ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category'])
41  
-        """
42  
-        rating_range, options = rating_string.split('|', 1)
43  
-        rating_range = range(int(rating_range[6:].split('-')[0]), int(rating_range[6:].split('-')[1])+1)
44  
-        choices = [c.replace('_', ' ') for c in options.split('|')]
45  
-        return rating_range, choices
46  
-
47  
-    def get_list_with_karma(self, **kwargs):
48  
-        """
49  
-        Returns a list of Comment objects matching the given lookup terms, with
50  
-        _karma_total_good and _karma_total_bad filled.
51  
-        """
52  
-        extra_kwargs = {}
53  
-        extra_kwargs.setdefault('select', {})
54  
-        extra_kwargs['select']['_karma_total_good'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=1'
55  
-        extra_kwargs['select']['_karma_total_bad'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=-1'
56  
-        return self.filter(**kwargs).extra(**extra_kwargs)
  12
+COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000)
57 13
 
58  
-    def user_is_moderator(self, user):
59  
-        if user.is_superuser:
60  
-            return True
61  
-        for g in user.groups.all():
62  
-            if g.id == settings.COMMENTS_MODERATORS_GROUP:
63  
-                return True
64  
-        return False
  14
+class BaseCommentAbstractModel(models.Model):
  15
+    """
  16
+    An abstract base class that any custom comment models probably should
  17
+    subclass.
  18
+    """
  19
+    
  20
+    # Content-object field
  21
+    content_type   = models.ForeignKey(ContentType)
  22
+    object_pk      = models.TextField(_('object ID'))
  23
+    content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
65 24
 
66  
-
67  
-class Comment(models.Model):
68  
-    """A comment by a registered user."""
69  
-    user = models.ForeignKey(User)
70  
-    content_type = models.ForeignKey(ContentType)
71  
-    object_id = models.IntegerField(_('object ID'))
72  
-    headline = models.CharField(_('headline'), max_length=255, blank=True)
73  
-    comment = models.TextField(_('comment'), max_length=3000)
74  
-    rating1 = models.PositiveSmallIntegerField(_('rating #1'), blank=True, null=True)
75  
-    rating2 = models.PositiveSmallIntegerField(_('rating #2'), blank=True, null=True)
76  
-    rating3 = models.PositiveSmallIntegerField(_('rating #3'), blank=True, null=True)
77  
-    rating4 = models.PositiveSmallIntegerField(_('rating #4'), blank=True, null=True)
78  
-    rating5 = models.PositiveSmallIntegerField(_('rating #5'), blank=True, null=True)
79  
-    rating6 = models.PositiveSmallIntegerField(_('rating #6'), blank=True, null=True)
80  
-    rating7 = models.PositiveSmallIntegerField(_('rating #7'), blank=True, null=True)
81  
-    rating8 = models.PositiveSmallIntegerField(_('rating #8'), blank=True, null=True)
82  
-    # This field designates whether to use this row's ratings in aggregate
83  
-    # functions (summaries). We need this because people are allowed to post
84  
-    # multiple reviews on the same thing, but the system will only use the
85  
-    # latest one (with valid_rating=True) in tallying the reviews.
86  
-    valid_rating = models.BooleanField(_('is valid rating'))
87  
-    submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True)
88  
-    is_public = models.BooleanField(_('is public'))
89  
-    ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
90  
-    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.'))
91  
-    site = models.ForeignKey(Site)
92  
-    objects = CommentManager()
  25
+    # Metadata about the comment
  26
+    site        = models.ForeignKey(Site)
93 27
 
94 28
     class Meta:
95  
-        verbose_name = _('comment')
96  
-        verbose_name_plural = _('comments')
97  
-        ordering = ('-submit_date',)
98  
-
99  
-    def __unicode__(self):
100  
-        return "%s: %s..." % (self.user.username, self.comment[:100])
  29
+        abstract = True
101 30
 
102  
-    def get_absolute_url(self):
103  
-        try:
104  
-            return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
105  
-        except AttributeError:
106  
-            return ""
107  
-
108  
-    def get_crossdomain_url(self):
109  
-        return "/r/%d/%d/" % (self.content_type_id, self.object_id)
110  
-
111  
-    def get_flag_url(self):
112  
-        return "/comments/flag/%s/" % self.id
113  
-
114  
-    def get_deletion_url(self):
115  
-        return "/comments/delete/%s/" % self.id
116  
-
117  
-    def get_content_object(self):
  31
+    def get_content_object_url(self):
118 32
         """
119  
-        Returns the object that this comment is a comment on. Returns None if
120  
-        the object no longer exists.
  33
+        Get a URL suitable for redirecting to the content object. Uses the
  34
+        ``django.views.defaults.shortcut`` view, which thus must be installed.
121 35
         """
122  
-        from django.core.exceptions import ObjectDoesNotExist
123  
-        try:
124  
-            return self.content_type.get_object_for_this_type(pk=self.object_id)
125  
-        except ObjectDoesNotExist:
126  
-            return None
127  
-
128  
-    get_content_object.short_description = _('Content object')
129  
-
130  
-    def _fill_karma_cache(self):
131  
-        """Helper function that populates good/bad karma caches."""
132  
-        good, bad = 0, 0
133  
-        for k in self.karmascore_set:
134  
-            if k.score == -1:
135  
-                bad +=1
136  
-            elif k.score == 1:
137  
-                good +=1
138  
-        self._karma_total_good, self._karma_total_bad = good, bad
139  
-
140  
-    def get_good_karma_total(self):
141  
-        if not hasattr(self, "_karma_total_good"):
142  
-            self._fill_karma_cache()
143  
-        return self._karma_total_good
144  
-
145  
-    def get_bad_karma_total(self):
146  
-        if not hasattr(self, "_karma_total_bad"):
147  
-            self._fill_karma_cache()
148  
-        return self._karma_total_bad
149  
-
150  
-    def get_karma_total(self):
151  
-        if not hasattr(self, "_karma_total_good") or not hasattr(self, "_karma_total_bad"):
152  
-            self._fill_karma_cache()
153  
-        return self._karma_total_good + self._karma_total_bad
154  
-
155  
-    def get_as_text(self):
156  
-        return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % \
157  
-            {'user': self.user.username, 'date': self.submit_date,
158  
-            'comment': self.comment, 'domain': self.site.domain, 'url': self.get_absolute_url()}
159  
-
160  
-
161  
-class FreeComment(models.Model):
162  
-    """A comment by a non-registered user."""
163  
-    content_type = models.ForeignKey(ContentType)
164  
-    object_id = models.IntegerField(_('object ID'))
165  
-    comment = models.TextField(_('comment'), max_length=3000)
166  
-    person_name = models.CharField(_("person's name"), max_length=50)
167  
-    submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True)
168  
-    is_public = models.BooleanField(_('is public'))
169  
-    ip_address = models.IPAddressField(_('ip address'))
170  
-    # TODO: Change this to is_removed, like Comment
171  
-    approved = models.BooleanField(_('approved by staff'))
172  
-    site = models.ForeignKey(Site)
  36
+        return urlresolvers.reverse(
  37
+            "django.views.defaults.shortcut",
  38
+            args=(self.content_type_id, self.object_pk)
  39
+        )
  40
+
  41
+class Comment(BaseCommentAbstractModel):
  42
+    """
  43
+    A user comment about some object.
  44
+    """
  45
+
  46
+    # Who posted this comment? If ``user`` is set then it was an authenticated
  47
+    # user; otherwise at least person_name should have been set and the comment
  48
+    # was posted by a non-authenticated user.
  49
+    user        = models.ForeignKey(User, blank=True, null=True, related_name="%(class)s_comments")
  50
+    user_name   = models.CharField(_("user's name"), max_length=50, blank=True)
  51
+    user_email  = models.EmailField(_("user's email address"), blank=True)
  52
+    user_url    = models.URLField(_("user's URL"), blank=True)
  53
+
  54
+    comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
  55
+
  56
+    # Metadata about the comment
  57
+    submit_date = models.DateTimeField(_('date/time submitted'), default=None)
  58
+    ip_address  = models.IPAddressField(_('IP address'), blank=True, null=True)
  59
+    is_public   = models.BooleanField(_('is public'), default=True,
  60
+                    help_text=_('Uncheck this box to make the comment effectively ' \
  61
+                                'disappear from the site.'))
  62
+    is_removed  = models.BooleanField(_('is removed'), default=False,
  63
+                    help_text=_('Check this box if the comment is inappropriate. ' \
  64
+                                'A "This comment has been removed" message will ' \
  65
+                                'be displayed instead.'))
  66
+
  67
+    # Manager
  68
+    objects = CommentManager()
173 69
 
174 70
     class Meta:
175  
-        verbose_name = _('free comment')
176  
-        verbose_name_plural = _('free comments')
177  
-        ordering = ('-submit_date',)
  71
+        db_table = "django_comments"
  72
+        ordering = ('submit_date',)
  73
+        permissions = [("can_moderate", "Can moderate comments")]
178 74
 
179 75
     def __unicode__(self):
180  
-        return "%s: %s..." % (self.person_name, self.comment[:100])
  76
+        return "%s: %s..." % (self.name, self.comment[:50])
181 77
 
182  
-    def get_absolute_url(self):
183  
-        try:
184  
-            return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
185  
-        except AttributeError:
186  
-            return ""
  78
+    def save(self):
  79
+        if self.submit_date is None:
  80
+            self.submit_date = datetime.datetime.now()
  81
+        super(Comment, self).save()
187 82
 
188  
-    def get_content_object(self):
  83
+    def _get_userinfo(self):
189 84
         """
190  
-        Returns the object that this comment is a comment on. Returns None if
191  
-        the object no longer exists.
192  
-        """
193  
-        from django.core.exceptions import ObjectDoesNotExist
194  
-        try:
195  
-            return self.content_type.get_object_for_this_type(pk=self.object_id)
196  
-        except ObjectDoesNotExist:
197  
-            return None
198  
-
199  
-    get_content_object.short_description = _('Content object')
200  
-
201  
-
202  
-class KarmaScoreManager(models.Manager):
203  
-    def vote(self, user_id, comment_id, score):
204  
-        try:
205  
-            karma = self.get(comment__pk=comment_id, user__pk=user_id)
206  
-        except self.model.DoesNotExist:
207  
-            karma = self.model(None, user_id=user_id, comment_id=comment_id, score=score, scored_date=datetime.datetime.now())
208  
-            karma.save()
209  
-        else:
210  
-            karma.score = score
211  
-            karma.scored_date = datetime.datetime.now()
212  
-            karma.save()
  85
+        Get a dictionary that pulls together information about the poster
  86
+        safely for both authenticated and non-authenticated comments.
213 87
 
214  
-    def get_pretty_score(self, score):
  88
+        This dict will have ``name``, ``email``, and ``url`` fields.
215 89
         """
216  
-        Given a score between -1 and 1 (inclusive), returns the same score on a
217  
-        scale between 1 and 10 (inclusive), as an integer.
218  
-        """
219  
-        if score is None:
220  
-            return DEFAULT_KARMA
221  
-        return int(round((4.5 * score) + 5.5))
222  
-
223  
-
224  
-class KarmaScore(models.Model):
225  
-    user = models.ForeignKey(User)
226  
-    comment = models.ForeignKey(Comment)
227  
-    score = models.SmallIntegerField(_('score'), db_index=True)
228  
-    scored_date = models.DateTimeField(_('score date'), auto_now=True)
229  
-    objects = KarmaScoreManager()
  90
+        if not hasattr(self, "_userinfo"):
  91
+            self._userinfo = {
  92
+                "name"  : self.user_name,
  93
+                "email" : self.user_email,
  94
+                "url"   : self.user_url
  95
+            }
  96
+            if self.user_id:
  97
+                u = self.user
  98
+                if u.email:
  99
+                    self._userinfo["email"] = u.email
  100
+
  101
+                # If the user has a full name, use that for the user name.
  102
+                # However, a given user_name overrides the raw user.username,
  103
+                # so only use that if this comment has no associated name.
  104
+                if u.get_full_name():
  105
+                    self._userinfo["name"] = self.user.get_full_name()
  106
+                elif not self.user_name:
  107
+                    self._userinfo["name"] = u.username
  108
+        return self._userinfo
  109
+    userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__)
  110
+
  111
+    def _get_name(self):
  112
+        return self.userinfo["name"]
  113
+    def _set_name(self, val):
  114
+        if self.user_id:
  115
+            raise AttributeError(_("This comment was posted by an authenticated "\
  116
+                                   "user and thus the name is read-only."))
  117
+        self.user_name = val
  118
+    name = property(_get_name, _set_name, doc="The name of the user who posted this comment")
  119
+
  120
+    def _get_email(self):
  121
+        return self.userinfo["email"]
  122
+    def _set_email(self, val):
  123
+        if self.user_id:
  124
+            raise AttributeError(_("This comment was posted by an authenticated "\
  125
+                                   "user and thus the email is read-only."))
  126
+        self.user_email = val
  127
+    email = property(_get_email, _set_email, doc="The email of the user who posted this comment")
  128
+
  129
+    def _get_url(self):
  130
+        return self.userinfo["url"]
  131
+    def _set_url(self, val):
  132
+        self.user_url = val
  133
+    url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment")
  134
+
  135
+    def get_absolute_url(self, anchor_pattern="#c%(id)s"):
  136
+        return self.get_content_object_url() + (anchor_pattern % self.__dict__)
230 137
 
231  
-    class Meta:
232  
-        verbose_name = _('karma score')
233  
-        verbose_name_plural = _('karma scores')
234  
-        unique_together = (('user', 'comment'),)
235  
-
236  
-    def __unicode__(self):
237  
-        return _("%(score)d rating by %(user)s") % {'score': self.score, 'user': self.user}
238  
-
239  
-
240  
-class UserFlagManager(models.Manager):
241  
-    def flag(self, comment, user):
  138
+    def get_as_text(self):
242 139
         """
243  
-        Flags the given comment by the given user. If the comment has already
244  
-        been flagged by the user, or it was a comment posted by the user,
245  
-        nothing happens.
  140
+        Return this comment as plain text.  Useful for emails.
246 141
         """
247  
-        if int(comment.user_id) == int(user.id):
248  
-            return # A user can't flag his own comment. Fail silently.
249  
-        try:
250  
-            f = self.get(user__pk=user.id, comment__pk=comment.id)
251  
-        except self.model.DoesNotExist:
252  
-            from django.core.mail import mail_managers
253  
-            f = self.model(None, user.id, comment.id, None)
254  
-            message = _('This comment was flagged by %(user)s:\n\n%(text)s') % {'user': user.username, 'text': comment.get_as_text()}
255  
-            mail_managers('Comment flagged', message, fail_silently=True)
256  
-            f.save()
257  
-
258  
-
259  
-class UserFlag(models.Model):
260  
-    user = models.ForeignKey(User)
261  
-    comment = models.ForeignKey(Comment)
262  
-    flag_date = models.DateTimeField(_('flag date'), auto_now_add=True)
263  
-    objects = UserFlagManager()
  142
+        d = {
  143
+            'user': self.user,
  144
+            'date': self.submit_date,
  145
+            'comment': self.comment,
  146
+            'domain': self.site.domain,
  147
+            'url': self.get_absolute_url()
  148
+        }
  149
+        return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
  150
+
  151
+class CommentFlag(models.Model):
  152
+    """
  153
+    Records a flag on a comment. This is intentionally flexible; right now, a
  154
+    flag could be:
  155
+
  156
+        * A "removal suggestion" -- where a user suggests a comment for (potential) removal.
  157
+
  158
+        * A "moderator deletion" -- used when a moderator deletes a comment.
  159
+
  160
+    You can (ab)use this model to add other flags, if needed. However, by
  161
+    design users are only allowed to flag a comment with a given flag once;
  162
+    if you want rating look elsewhere.
  163
+    """
  164
+    user      = models.ForeignKey(User, related_name="comment_flags")
  165
+    comment   = models.ForeignKey(Comment, related_name="flags")
  166
+    flag      = models.CharField(max_length=30, db_index=True)
  167
+    flag_date = models.DateTimeField(default=None)
  168
+
  169
+    # Constants for flag types
  170
+    SUGGEST_REMOVAL = "removal suggestion"
  171
+    MODERATOR_DELETION = "moderator deletion"
  172
+    MODERATOR_APPROVAL = "moderator approval"
264 173
 
265 174
     class Meta:
266  
-        verbose_name = _('user flag')
267  
-        verbose_name_plural = _('user flags')
268  
-        unique_together = (('user', 'comment'),)
  175
+        db_table = 'django_comment_flags'
  176
+        unique_together = [('user', 'comment', 'flag')]
269 177
 
270 178
     def __unicode__(self):
271  
-        return _("Flag by %r") % self.user
272  
-
273  
-
274  
-class ModeratorDeletion(models.Model):
275  
-    user = models.ForeignKey(User, verbose_name='moderator')
276  
-    comment = models.ForeignKey(Comment)
277  
-    deletion_date = models.DateTimeField(_('deletion date'), auto_now_add=True)
  179
+        return "%s flag of comment ID %s by %s" % \
  180
+            (self.flag, self.comment_id, self.user.username)
278 181
 
279  
-    class Meta:
280  
-        verbose_name = _('moderator deletion')
281  
-        verbose_name_plural = _('moderator deletions')
282  
-        unique_together = (('user', 'comment'),)
283  
-
284  
-    def __unicode__(self):
285  
-        return _("Moderator deletion by %r") % self.user
286  
-        
  182
+    def save(self):
  183
+        if self.flag_date is None:
  184
+            self.flag_date = datetime.datetime.now()
  185
+        super(CommentFlag, self).save()
21  django/contrib/comments/signals.py
... ...
@@ -0,0 +1,21 @@
  1
+"""
  2
+Signals relating to comments.
  3
+"""
  4
+from django.dispatch import Signal
  5
+
  6
+# Sent just before a comment will be posted (after it's been approved and
  7
+# moderated; this can be used to modify the comment (in place) with posting
  8
+# details or other such actions. If any receiver returns False the comment will be
  9
+# discarded and a 403 (not allowed) response. This signal is sent at more or less
  10
+# the same time (just before, actually) as the Comment object's pre-save signal,
  11
+# except that the HTTP request is sent along with this signal.
  12
+comment_will_be_posted = Signal()
  13
+
  14
+# Sent just after a comment was posted. See above for how this differs
  15
+# from the Comment object's post-save signal.
  16
+comment_was_posted = Signal()
  17
+
  18
+# Sent after a comment was "flagged" in some way. Check the flag to see if this
  19
+# was a user requesting removal of a comment, a moderator approving/removing a
  20
+# comment, or some other custom user flag.
  21
+comment_was_flagged = Signal()
53  django/contrib/comments/templates/comments/400-debug.html
... ...
@@ -0,0 +1,53 @@
  1
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  2
+<html lang="en">
  3
+<head>
  4
+  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  5
+  <title>Comment post not allowed (400)</title>
  6
+  <meta name="robots" content="NONE,NOARCHIVE" />
  7
+  <style type="text/css">
  8
+    html * { padding:0; margin:0; }
  9
+    body * { padding:10px 20px; }
  10
+    body * * { padding:0; }
  11
+    body { font:small sans-serif; background:#eee; }
  12
+    body>div { border-bottom:1px solid #ddd; }
  13
+    h1 { font-weight:normal; margin-bottom:.4em; }
  14
+    h1 span { font-size:60%; color:#666; font-weight:normal; }
  15
+    table { border:none; border-collapse: collapse; width:100%; }
  16
+    td, th { vertical-align:top; padding:2px 3px; }
  17
+    th { width:12em; text-align:right; color:#666; padding-right:.5em; }
  18
+    #info { background:#f6f6f6; }
  19
+    #info ol { margin: 0.5em 4em; }
  20
+    #info ol li { font-family: monospace; }
  21
+    #summary { background: #ffc; }
  22
+    #explanation { background:#eee; border-bottom: 0px none; }
  23
+  </style>
  24
+</head>
  25
+<body>
  26
+  <div id="summary">
  27
+    <h1>Comment post not allowed <span>(400)</span></h1>
  28
+    <table class="meta">
  29
+      <tr>
  30
+        <th>Why:</th>
  31
+        <td>{{ why }}</td>
  32
+      </tr>
  33
+    </table>
  34
+  </div>
  35
+  <div id="info">
  36
+    <p>
  37
+    The comment you tried to post to this view wasn't saved because something
  38
+    tampered with the security information in the comment form. The message
  39
+    above should explain the problem, or you can check the <a
  40
+    href="http://www.djangoproject.com/documentation/comments/">comment
  41
+    documentation</a> for more help.
  42
+    </p>
  43
+  </div>
  44
+
  45
+  <div id="explanation">
  46
+    <p>
  47
+      You're seeing this error because you have <code>DEBUG = True</code> in
  48
+      your Django settings file. Change that to <code>False</code>, and Django
  49
+      will display a standard 400 error page.
  50
+    </p>
  51
+  </div>
  52
+</body>
  53
+</html>
14  django/contrib/comments/templates/comments/approve.html
... ...
@@ -0,0 +1,14 @@
  1
+{% extends "comments/base.html" %}
  2
+
  3
+{% block title %}Approve a comment{% endblock %}
  4
+
  5
+{% block content %}
  6
+  <h1>Really make this comment public?</h1>
  7
+  <blockquote>{{ comment|escape|linebreaks }}</blockquote>
  8
+  <form action="." method="POST">
  9
+    <input type="hidden" name="next" value="{{ next|escape }}" id="next">
  10
+    <p class="submit">
  11
+      <input type="submit" name="submit" value="Approve"> or <a href="{{ comment.permalink }}">cancel</a>
  12
+    </p>
  13
+  </form>
  14
+{% endblock %}
7  django/contrib/comments/templates/comments/approved.html
... ...
@@ -0,0 +1,7 @@
  1
+{% extends "comments/base.html" %}
  2
+
  3
+{% block title %}Thanks for approving.{% endblock %}
  4
+
  5
+{% block content %}
  6
+  <h1>Thanks for taking the time to improve the quality of discussion on our site.</h1>
  7
+{% endblock %}
10  django/contrib/comments/templates/comments/base.html
... ...
@@ -0,0 +1,10 @@
  1
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
  2
+<html lang="en">
  3
+<head>
  4
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  5
+  <title>{% block title %}{% endblock %}</title>
  6
+</head>
  7
+<body>
  8
+  {% block content %}{% endblock %}
  9
+</body>
  10
+</html>
14  django/contrib/comments/templates/comments/delete.html
... ...
@@ -0,0 +1,14 @@
  1
+{% extends "comments/base.html" %}
  2
+
  3
+{% block title %}Remove a comment{% endblock %}
  4
+
  5
+{% block content %}
  6
+  <h1>Really remove this comment?</h1>
  7
+  <blockquote>{{ comment|escape|linebreaks }}</blockquote>
  8
+  <form action="." method="POST">
  9
+    <input type="hidden" name="next" value="{{ next|escape }}" id="next">
  10
+    <p class="submit">
  11
+      <input type="submit" name="submit" value="Remove"> or <a href="{{ comment.permalink }}">cancel</a>
  12
+    </p>
  13
+  </form>
  14
+{% endblock %}
7  django/contrib/comments/templates/comments/deleted.html
... ...
@@ -0,0 +1,7 @@
  1
+{% extends "comments/base.html" %}
  2
+
  3
+{% block title %}Thanks for removing.{% endblock %}
  4
+
  5
+{% block content %}
  6
+  <h1>Thanks for taking the time to improve the quality of discussion on our site.</h1>
  7
+{% endblock %}
14  django/contrib/comments/templates/comments/flag.html
... ...
@@ -0,0 +1,14 @@
  1
+{% extends "comments/base.html" %}
  2
+
  3
+{% block title %}Flag this comment{% endblock %}
  4
+
  5
+{% block content %}
  6
+  <h1>Really flag this comment?</h1>
  7
+  <blockquote>{{ comment|escape|linebreaks }}</blockquote>
  8
+  <form action="." method="POST">
  9
+    <input type="hidden" name="next" value="{{ next|escape }}" id="next">
  10
+    <p class="submit">
  11
+      <input type="submit" name="submit" value="Flag"> or <a href="{{ comment.permalink }}">cancel</a>
  12
+    </p>
  13
+  </form>
  14
+{% endblock %}
7  django/contrib/comments/templates/comments/flagged.html
... ...
@@ -0,0 +1,7 @@
  1
+{% extends "comments/base.html" %}
  2
+
  3
+{% block title %}Thanks for flagging.{% endblock %}
  4
+
  5
+{% block content %}
  6
+  <h1>Thanks for taking the time to improve the quality of discussion on our site.</h1>
  7
+{% endblock %}
57  django/contrib/comments/templates/comments/form.html
... ...
@@ -1,38 +1,19 @@
1  
-{% load i18n %}
2  
-{% if display_form %}
3  
-<form {% if photos_optional or photos_required %}enctype="multipart/form-data" {% endif %}action="/comments/post/" method="post">
4  
-
5  
-{% if user.is_authenticated %}
6  
-<p>{% trans "Username:" %} <strong>{{ user.username }}</strong> (<a href="{{ logout_url }}">{% trans "Log out" %}</a>)</p>
7  
-{% else %}
8  
-<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>
9  
-{% endif %}
10  
-
11  
-{% if ratings_optional or ratings_required %}
12  
-<p>{% trans "Ratings" %} ({% if ratings_required %}{% trans "Required" %}{% else %}{% trans "Optional" %}{% endif %}):</p>
13  
-<table>
14  
-<tr><th>&nbsp;</th>{% for value in rating_range %}<th>{{ value }}</th>{% endfor %}</tr>
15  
-{% for rating in rating_choices %}
16  
-<tr><th>{{ rating }}</th>{% for value in rating_range %}<th><input type="radio" name="rating{{ forloop.parentloop.counter }}" value="{{ value }}" /></th>{% endfor %}</tr>
17  
-{% endfor %}
18  
-</table>
19  
-<input type="hidden" name="rating_options" value="{{ rating_options }}" />
20  
-{% endif %}
21  
-
22  
-{% if photos_optional or photos_required %}
23  
-<p><label for="id_photo">{% trans "Post a photo" %}</label> ({% if photos_required %}{% trans "Required" %}{% else %}{% trans "Optional" %}{% endif %}):
24  
-<input type="file" name="photo" id="id_photo" /></p>
25  
-<input type="hidden" name="photo_options" value="{{ photo_options }}" />
26  
-{% endif %}
27  
-
28  
-<p><label for="id_comment">{% trans "Comment:" %}</label><br />
29  
-<textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
30  
-
31  
-<p>
32  
-<input type="hidden" name="options" value="{{ options }}" />
33  
-<input type="hidden" name="target" value="{{ target }}" />
34  
-<input type="hidden" name="gonzo" value="{{ hash }}" />
35  
-<input type="submit" name="preview" value="{% trans "Preview comment" %}" />
36  
-</p>
37  
-</form>
38  
-{% endif %}
  1
+{% load comments %}
  2
+<form action="{% comment_form_target %}" method="POST">
  3
+  {% for field in form %}
  4
+    {% if field.is_hidden %}
  5
+      {{ field }}
  6
+    {% else %}
  7
+      <p
  8
+        {% if field.errors %} class="error"{% endif %}
  9
+        {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
  10
+        {% if field.errors %}{{ field.errors }}{% endif %}
  11
+        {{ field.label_tag }} {{ field }}
  12
+      </p>
  13
+    {% endif %}
  14
+  {% endfor %}
  15
+  <p class="submit">
  16
+    <input type="submit" name="submit" class="submit-post" value="Post">
  17
+    <input type="submit" name="submit" class="submit-preview" value="Preview">
  18
+  </p>
  19
+</form>
13  django/contrib/comments/templates/comments/freeform.html
... ...
@@ -1,13 +0,0 @@
1  
-{% load i18n %}
2  
-{% if display_form %}
3  
-<form action="/comments/postfree/" method="post">
4  
-<p><label for="id_person_name">{% trans "Your name:" %}</label> <input type="text" id="id_person_name" name="person_name" /></p>
5  
-<p><label for="id_comment">{% trans "Comment:" %}</label><br /><textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
6  
-<p>
7  
-<input type="hidden" name="options" value="{{ options }}" />
8  
-<input type="hidden" name="target" value="{{ target }}" />