Changeset 8557
- Timestamp:
- 08/25/08 17:14:22 (10 months ago)
- Files:
-
- django/trunk/AUTHORS (modified) (1 diff)
- django/trunk/django/contrib/comments/admin.py (modified) (1 diff)
- django/trunk/django/contrib/comments/feeds.py (modified) (2 diffs)
- django/trunk/django/contrib/comments/forms.py (added)
- django/trunk/django/contrib/comments/__init__.py (modified) (1 diff)
- django/trunk/django/contrib/comments/managers.py (added)
- django/trunk/django/contrib/comments/models.py (modified) (1 diff)
- django/trunk/django/contrib/comments/signals.py (added)
- django/trunk/django/contrib/comments/templates/comments/400-debug.html (added)
- django/trunk/django/contrib/comments/templates/comments/approved.html (added)
- django/trunk/django/contrib/comments/templates/comments/approve.html (added)
- django/trunk/django/contrib/comments/templates/comments/base.html (added)
- django/trunk/django/contrib/comments/templates/comments/deleted.html (added)
- django/trunk/django/contrib/comments/templates/comments/delete.html (added)
- django/trunk/django/contrib/comments/templates/comments/flagged.html (added)
- django/trunk/django/contrib/comments/templates/comments/flag.html (added)
- django/trunk/django/contrib/comments/templates/comments/form.html (modified) (1 diff)
- django/trunk/django/contrib/comments/templates/comments/freeform.html (deleted)
- django/trunk/django/contrib/comments/templates/comments/moderation_queue.html (added)
- django/trunk/django/contrib/comments/templates/comments/posted.html (added)
- django/trunk/django/contrib/comments/templates/comments/preview.html (added)
- django/trunk/django/contrib/comments/templates/comments/reply.html (added)
- django/trunk/django/contrib/comments/templates/comments/reply_preview.html (added)
- django/trunk/django/contrib/comments/templatetags/comments.py (modified) (1 diff)
- django/trunk/django/contrib/comments/tests.py (deleted)
- django/trunk/django/contrib/comments/urls/comments.py (deleted)
- django/trunk/django/contrib/comments/urls.py (added)
- django/trunk/django/contrib/comments/views/comments.py (modified) (1 diff)
- django/trunk/django/contrib/comments/views/karma.py (deleted)
- django/trunk/django/contrib/comments/views/moderation.py (added)
- django/trunk/django/contrib/comments/views/userflags.py (deleted)
- django/trunk/django/contrib/comments/views/utils.py (added)
- django/trunk/docs/index.txt (modified) (4 diffs)
- django/trunk/docs/ref/contrib/comments (added)
- django/trunk/docs/ref/contrib/comments/index.txt (added)
- django/trunk/docs/ref/contrib/comments/settings.txt (added)
- django/trunk/docs/ref/contrib/comments/upgrade.txt (added)
- django/trunk/docs/ref/contrib/index.txt (modified) (2 diffs)
- django/trunk/docs/_static/djangodocs.css (modified) (1 diff)
- django/trunk/docs/topics/templates.txt (modified) (1 diff)
- django/trunk/tests/regressiontests/comment_tests (added)
- django/trunk/tests/regressiontests/comment_tests/fixtures (added)
- django/trunk/tests/regressiontests/comment_tests/fixtures/comment_tests.json (added)
- django/trunk/tests/regressiontests/comment_tests/__init__.py (moved) (moved from django/trunk/django/contrib/comments/urls/__init__.py)
- django/trunk/tests/regressiontests/comment_tests/models.py (added)
- django/trunk/tests/regressiontests/comment_tests/tests (added)
- django/trunk/tests/regressiontests/comment_tests/tests/app_api_tests.py (added)
- django/trunk/tests/regressiontests/comment_tests/tests/comment_form_tests.py (added)
- django/trunk/tests/regressiontests/comment_tests/tests/comment_view_tests.py (added)
- django/trunk/tests/regressiontests/comment_tests/tests/__init__.py (added)
- django/trunk/tests/regressiontests/comment_tests/tests/model_tests.py (added)
- django/trunk/tests/regressiontests/comment_tests/tests/moderation_view_tests.py (added)
- django/trunk/tests/regressiontests/comment_tests/tests/templatetag_tests.py (added)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
django/trunk/AUTHORS
r8539 r8557 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 django/trunk/django/contrib/comments/admin.py
r7967 r8557 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 6 class CommentsAdmin(admin.ModelAdmin): 7 fieldsets = ( 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 ) 4 18 5 class CommentAdmin(admin.ModelAdmin): 6 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',) 19 list_display = ('name', 'content_type', 'object_pk', 'ip_address', 'is_public', 'is_removed') 20 list_filter = ('submit_date', 'site', 'is_public', 'is_removed') 14 21 date_hierarchy = 'submit_date' 15 search_fields = ('comment', 'user__username') 16 raw_id_fields = ('user',) 22 search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') 17 23 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',) 26 date_hierarchy = 'submit_date' 27 search_fields = ('comment', 'person_name') 28 29 admin.site.register(Comment, CommentAdmin) 30 admin.site.register(FreeComment, FreeCommentAdmin) 24 admin.site.register(Comment, CommentsAdmin) django/trunk/django/contrib/comments/feeds.py
r6529 r8557 1 1 from django.conf import settings 2 from django.contrib.comments.models import Comment, FreeComment3 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): … … 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 django/trunk/django/contrib/comments/__init__.py
r4265 r8557 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/trunk/django/contrib/comments/models.py
r8193 r8557 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 COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000) 12 13 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' 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") 19 24 20 # What users get if they don't have any karma. 21 DEFAULT_KARMA = 5 22 KARMA_NEEDED_BEFORE_DISPLAYED = 3 25 # Metadata about the comment 26 site = models.ForeignKey(Site) 23 27 28 class Meta: 29 abstract = True 24 30 25 class CommentManager(models.Manager): 26 def get_security_hash(self, options, photo_options, rating_options, target): 31 def get_content_object_url(self): 27 32 """ 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. 33 Get a URL suitable for redirecting to the content object. Uses the 34 ``django.views.defaults.shortcut`` view, which thus must be installed. 31 35 """ 32 from django.utils.hashcompat import md5_constructor 33 return md5_constructor(options + photo_options + rating_options + target + settings.SECRET_KEY).hexdigest() 36 return urlresolvers.reverse( 37 "django.views.defaults.shortcut", 38 args=(self.content_type_id, self.object_pk) 39 ) 34 40 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 41 class Comment(BaseCommentAbstractModel): 42 """ 43 A user comment about some object. 44 """ 46 45 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) 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) 57 53 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 54 comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) 65 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 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) 67 # Manager 92 68 objects = CommentManager() 93 69 94 70 class Meta: 95 verbose_name = _('comment')96 verbose_name_plural = _('comments')97 ordering = ('-submit_date',)71 db_table = "django_comments" 72 ordering = ('submit_date',) 73 permissions = [("can_moderate", "Can moderate comments")] 98 74 99 75 def __unicode__(self): 100 return "%s: %s..." % (self. user.username, self.comment[:100])76 return "%s: %s..." % (self.name, self.comment[:50]) 101 77 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 "" 78 def save(self): 79 if self.submit_date is None: 80 self.submit_date = datetime.datetime.now() 81 super(Comment, self).save() 107 82 108 def get_crossdomain_url(self): 109 return "/r/%d/%d/" % (self.content_type_id, self.object_id) 83 def _get_userinfo(self): 84 """ 85 Get a dictionary that pulls together information about the poster 86 safely for both authenticated and non-authenticated comments. 110 87 111 def get_flag_url(self): 112 return "/comments/flag/%s/" % self.id 88 This dict will have ``name``, ``email``, and ``url`` fields. 89 """ 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 113 100 114 def get_deletion_url(self): 115 return "/comments/delete/%s/" % self.id 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__) 116 110 117 def get_content_object(self): 118 """ 119 Returns the object that this comment is a comment on. Returns None if 120 the object no longer exists. 121 """ 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 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") 127 119 128 get_content_object.short_description = _('Content object') 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") 129 128 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 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") 139 134 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 135 def get_absolute_url(self, anchor_pattern="#c%(id)s"): 136 return self.get_content_object_url() + (anchor_pattern % self.__dict__) 154 137 155 138 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()} 139 """ 140 Return this comment as plain text. Useful for emails. 141 """ 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 159 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: 160 155 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) 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" 173 173 174 174 class Meta: 175 verbose_name = _('free comment') 176 verbose_name_plural = _('free comments') 177 ordering = ('-submit_date',) 175 db_table = 'django_comment_flags' 176 unique_together = [('user', 'comment', 'flag')] 178 177 179 178 def __unicode__(self): 180 return "%s: %s..." % (self.person_name, self.comment[:100]) 179 return "%s flag of comment ID %s by %s" % \ 180 (self.flag, self.comment_id, self.user.username) 181 181 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 "" 187 188 def get_content_object(self): 189 """ 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() 213 214 def get_pretty_score(self, score): 215 """ 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() 230 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): 242 """ 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. 246 """ 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() 264 265 class Meta: 266 verbose_name = _('user flag') 267 verbose_name_plural = _('user flags') 268 unique_together = (('user', 'comment'),) 269 270 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) 278 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() django/trunk/django/contrib/comments/templates/comments/form.html
r7294 r8557 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> </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> 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> 37 19 </form> 38 {% endif %}django/trunk/django/contrib/comments/templatetags/comments.py
r6399 r8557 1 from django.contrib.comments.models import Comment, FreeComment2 from django.contrib.comments.models import PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC3 from django.contrib.comments.models import MIN_PHOTO_DIMENSION, MAX_PHOTO_DIMENSION4 1 from django import template 5 from django.template import loader6 from django.co re.exceptions import ObjectDoesNotExist2 from django.template.loader import render_to_string 3 from django.conf import settings 7 4 from django.contrib.contenttypes.models import ContentType 8 from django.utils.encoding import smart_str 9 import re 5 from django.contrib import comments 10 6 11 7 register = template.Library() 12 8 13 COMMENT_FORM = 'comments/form.html' 14 FREE_COMMENT_FORM = 'comments/freeform.html' 15 16 class CommentFormNode(template.Node): 17 def __init__(self, content_type, obj_id_lookup_var, obj_id, free, 18 photos_optional=False, photos_required=False, photo_options='', 19 ratings_optional=False, ratings_required=False, rating_options='', 20 is_public=True): 21 self.content_type = content_type 22 if obj_id_lookup_var is not None: 23 obj_id_lookup_var = template.Variable(obj_id_lookup_var) 24 self.obj_id_lookup_var, self.obj_id, self.free = obj_id_lookup_var, obj_id, free 25 self.photos_optional, self.photos_required = photos_optional, photos_required 26 self.ratings_optional, self.ratings_required = ratings_optional, ratings_required 27 self.photo_options, self.rating_options = photo_options, rating_options 28 self.is_public = is_public 9 class BaseCommentNode(template.Node): 10 """ 11 Base helper class (abstract) for handling the get_comment_* template tags. 12 Looks a bit strange, but the subclasses below should make this a bit more 13 obvious. 14 """ 15 16 #@classmethod 17 def handle_token(cls, parser, token): 18 """Class method to parse get_comment_list/count/form and return a Node.""" 19 tokens = token.contents.split() 20 if tokens[1] != 'for': 21 raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0]) 22 23 # {% get_whatever for obj as varname %} 24 if len(tokens) == 5: 25 if tokens[3] != 'as': 26 raise template.TemplateSyntaxError("Third argument in %r must be 'as'" % tokens[0]) 27 return cls( 28 object_expr = parser.compile_filter(tokens[2]), 29 as_varname = tokens[4], 30 ) 31 32 # {% get_whatever for app.model pk as varname %} 33 elif len(tokens) == 6: 34 if tokens[4] != 'as': 35 raise template.TemplateSyntaxError("Fourth argument in %r must be 'as'" % tokens[0]) 36 return cls( 37 ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]), 38 object_pk_expr = parser.compile_filter(tokens[3]), 39 as_varname = tokens[5] 40 ) 41 42 else: 43 raise template.TemplateSyntaxError("%r tag requires 4 or 5 arguments" % tokens[0]) 44 45 handle_token = classmethod(handle_token) 46 47 #@staticmethod 48 def lookup_content_type(token, tagname): 49 try: 50 app, model = token.split('.') 51 return ContentType.objects.get(app_label=app, model=model) 52 except ValueError: 53 raise template.TemplateSyntaxError("Third argument in %r must be in the format 'app.model'" % tagname) 54 except ContentType.DoesNotExist: 55 raise template.TemplateSyntaxError("%r tag has non-existant content-type: '%s.%s'" % (tagname, app, model)) 56 lookup_content_type = staticmethod(lookup_content_type) 57 58 def __init__(self, ctype=None, object_pk_expr=None, object_expr=None, as_varname=None, comment=None): 59 if ctype is None and object_expr is None: 60 raise template.TemplateSyntaxError("Comment nodes must be given either a literal object or a ctype and object pk.") 61 self.comment_model = comments.get_model() 62 self.as_varname = as_varname 63 self.ctype = ctype 64 self.object_pk_expr = object_pk_expr 65 self.object_expr = object_expr 66 self.comment = comment 29 67 30 68 def render(self, context): 31 from django.conf import settings 32 from django.utils.text import normalize_newlines 33 import base64 34 context.push() 35 if self.obj_id_lookup_var is not None: 69 qs = self.get_query_set(context) 70 context[self.as_varname] = self.get_context_value_from_queryset(context, qs) 71 return '' 72 73 def get_query_set(self, context): 74 ctype, object_pk = self.get_target_ctype_pk(context) 75 if not object_pk: 76 return self.comment_model.objects.none() 77 78 qs = self.comment_model.objects.filter( 79 content_type = ctype, 80 object_pk = object_pk, 81 site__pk = settings.SITE_ID, 82 is_public = True, 83 ) 84 if settings.COMMENTS_HIDE_REMOVED: 85 qs = qs.filter(is_removed=False) 86 87 return qs 88 89 def get_target_ctype_pk(self, context): 90 if self.object_expr: 36 91 try: 37 self.obj_id = self.obj_id_lookup_var.resolve(context)92 obj = self.object_expr.resolve(context) 38 93 except template.VariableDoesNotExist: 39 return '' 40 # Validate that this object ID is valid for this content-type. 41 # We only have to do this validation if obj_id_lookup_var is provided, 42 # because do_comment_form() validates hard-coded object IDs. 43 try: 44 self.content_type.get_object_for_this_type(pk=self.obj_id) 45 except ObjectDoesNotExist: 46 context['display_form'] = False 47 else: 48 context['display_form'] = True 49 else: 50 context['display_form'] = True 51 context['target'] = '%s:%s' % (self.content_type.id, self.obj_id) 52 options = [] 53 for var, abbr in (('photos_required', PHOTOS_REQUIRED), 54 ('photos_optional', PHOTOS_OPTIONAL), 55 ('ratings_required', RATINGS_REQUIRED), 56 ('ratings_optional', RATINGS_OPTIONAL), 57 ('is_public', IS_PUBLIC)): 58 context[var] = getattr(self, var) 59 if getattr(self, var): 60 options.append(abbr) 61 context['options'] = ','.join(options) 62 if self.free: 63 context['hash'] = Comment.objects.get_security_hash(context['options'], '', '', context['target']) 64 default_form = loader.get_template(FREE_COMMENT_FORM) 65 else: 66 context['photo_options'] = self.photo_options 67 context['rating_options'] = normalize_newlines(base64.encodestring(self.rating_options).strip()) 68 if self.rating_options: 69 context['rating_range'], context['rating_choices'] = Comment.objects.get_rating_options(self.rating_options) 70 context['hash'] = Comment.objects.get_security_hash(context['options'], context['photo_options'], context['rating_options'], context['target']) 71 context['logout_url'] = settings.LOGOUT_URL 72 default_form = loader.get_template(COMMENT_FORM) 73 output = default_form.render(context) 74 context.pop() 75 return output 76 77 class CommentCountNode(template.Node): 78 def __init__(self, package, module, context_var_name, obj_id, var_name, free): 79 self.package, self.module = package, module 80 if context_var_name is not None: 81 context_var_name = template.Variable(context_var_name) 82 self.context_var_name, self.obj_id = context_var_name, obj_id 83 self.var_name, self.free = var_name, free 94 return None, None 95 return ContentType.objects.get_for_model(obj), obj.pk 96 else: 97 return self.ctype, self.object_pk_expr.resolve(context, ignore_failures=True) 98 99 def get_context_value_from_queryset(self, context, qs): 100 """Subclasses should override this.""" 101 raise NotImplementedError 102 103 class CommentListNode(BaseCommentNode): 104 """Insert a list of comments into the context.""" 105 def get_context_value_from_queryset(self, context, qs): 106 return list(qs) 107 108 class CommentCountNode(BaseCommentNode): 109 """Insert a count of comments into the context.""" 110 def get_context_value_from_queryset(self, context, qs): 111 return qs.count() 112 113 class CommentFormNode(BaseCommentNode): 114 """Insert a form for the comment model into the context.""" 115 116 def get_form(self, context): 117 ctype, object_pk = self.get_target_ctype_pk(context) 118 if object_pk: 119 return comments.get_form()(ctype.get_object_for_this_type(pk=object_pk)) 120 else: 121 return None 84 122 85 123 def render(self, context): 86 from django.conf import settings 87 manager = self.free and FreeComment.objects or Comment.objects 88 if self.context_var_name is not None: 89 self.obj_id = self.context_var_name.resolve(context) 90 comment_count = manager.filter(object_id__exact=self.obj_id, 91 content_type__app_label__exact=self.package, 92 content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count() 93 context[self.var_name] = comment_count 124 context[self.as_varname] = self.get_form(context) 94 125 return '' 95 126 96 class CommentListNode(template.Node): 97 def __init__(self, package, module, context_var_name, obj_id, var_name, free, ordering, extra_kwargs=None): 98 self.package, self.module = package, module 99 if context_var_name is not None: 100 context_var_name = template.Variable(context_var_name) 101 self.context_var_name, self.obj_id = context_var_name, obj_id 102 self.var_name, self.free = var_name, free 103 self.ordering = ordering 104 self.extra_kwargs = extra_kwargs or {} 127 class RenderCommentFormNode(CommentFormNode): 128 """Render the comment form directly""" 129 130 #@classmethod 131 def handle_token(cls, parser, token): 132 """Class method to parse render_comment_form and return a Node.""" 133 tokens = token.contents.split() 134 if tokens[1] != 'for': 135 raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0]) 136 137 # {% render_comment_form for obj %} 138 if len(tokens) == 3: 139 return cls(object_expr=parser.compile_filter(tokens[2])) 140 141 # {% render_comment_form for app.models pk %} 142 elif len(tokens) == 4: 143 return cls( 144 ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]), 145 object_pk_expr = parser.compile_filter(tokens[3]) 146 ) 147 handle_token = classmethod(handle_token) 105 148 106 149 def render(self, context): 107 from django.conf import settings 108 get_list_function = self.free and FreeComment.objects.filter or Comment.objects.get_list_with_karma 109 if self.context_var_name is not None: 110 try: 111 self.obj_id = self.context_var_name.resolve(context) 112 except template.VariableDoesNotExist: 113 return '' 114 kwargs = { 115 'object_id__exact': self.obj_id, 116 'content_type__app_label__exact': self.package, 117 'content_type__model__exact': self.module, 118 'site__id__exact': settings.SITE_ID, 119 } 120 kwargs.update(self.extra_kwargs) 121 comment_list = get_list_function(**kwargs).order_by(self.ordering + 'submit_date').select_related() 122 if not self.free and settings.COMMENTS_BANNED_USERS_GROUP: 123 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}) 124 125 if not self.free: 126 if 'user' in context and context['user'].is_authenticated(): 127 user_id = context['user'].id 128 context['user_can_moderate_comments'] = Comment.objects.user_is_moderator(context['user']) 129 else: 130 user_id = None 131 context['user_can_moderate_comments'] = False 132 # Only display comments by banned users to those users themselves. 133 if settings.COMMENTS_BANNED_USERS_GROUP: 134 comment_list = [c for c in comment_list if not c.is_hidden or (user_id == c.user_id)] 135 136 context[self.var_name] = comment_list 137 return '' 138 139 class DoCommentForm: 140 """ 141 Displays a comment form for the given params. 142 143 Syntax:: 144 145 {% comment_form for [pkg].[py_module_name] [context_var_containing_obj_id] with [list of options] %} 150 ctype, object_pk = self.get_target_ctype_pk(context) 151 if object_pk: 152 template_search_list = [ 153 "comments/%s/%s/form.html" % (ctype.app_label, ctype.model), 154 "comments/%s/form.html" % ctype.app_label, 155 "comments/form.html" 156 ] 157 context.push() 158 formstr = render_to_string(template_search_list, {"form" : self.get_form(context)}, context) 159 context.pop() 160 return formstr 161 else: 162 return '' 163 164 # We could just register each classmethod directly, but then we'd lose out on 165 # the automagic docstrings-into-admin-docs tricks. So each node gets a cute 166 # wrapper function that just exists to hold the docstring. 167 168 #@register.tag 169 def get_comment_count(parser, token): 170 """ 171 Gets the comment count for the given params and populates the template 172 context with a variable containing that value, whose name is defined by the 173 'as' clause. 174 175 Syntax:: 176 177 {% get_comment_count for [object] as [varname] %} 178 {% get_comment_count for [app].[model] [object_id] as [varname] %} 146 179 147 180 Example usage:: 148 181 149 {% 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 %} 150 151 ``[context_var_containing_obj_id]`` can be a hard-coded integer or a variable containing the ID. 152 """ 153 def __init__(self, free): 154 self.free = free 155 156 def __call__(self, parser, token): 157 tokens = token.contents.split() 158 if len(tokens) < 4: 159 raise template.TemplateSyntaxError, "%r tag requires at least 3 arguments" % tokens[0] 160 if tokens[1] != 'for': 161 raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0] 162 try: 163 package, module = tokens[2].split('.') 164 except ValueError: # unpack list of wrong size 165 raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] 166 try: 167 content_type = ContentType.objects.get(app_label__exact=package, model__exact=module) 168 except ContentType.DoesNotExist: 169 raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) 170 obj_id_lookup_var, obj_id = None, None 171 if tokens[3].isdigit(): 172 obj_id = tokens[3] 173 try: # ensure the object ID is valid 174 content_type.get_object_for_this_type(pk=obj_id) 175 except ObjectDoesNotExist: 176 raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id) 177 else: 178 obj_id_lookup_var = tokens[3] 179 kwargs = {} 180 if len(tokens) > 4: 181 if tokens[4] != 'with': 182 raise template.TemplateSyntaxError, "Fourth argument in %r tag must be 'with'" % tokens[0] 183 for option, args in zip(tokens[5::2], tokens[6::2]): 184 option = smart_str(option) 185 if option in ('photos_optional', 'photos_required') and not self.free: 186 # VALIDATION ############################################## 187 option_list = args.split(',') 188 if len(option_list) % 3 != 0: 189 raise template.TemplateSyntaxError, "Incorrect number of comma-separated arguments to %r tag" % tokens[0] 190 for opt in option_list[::3]: 191 if not opt.isalnum(): 192 raise template.TemplateSyntaxError, "Invalid photo directory name in %r tag: '%s'" % (tokens[0], opt) 193 for opt in option_list[1::3] + option_list[2::3]: 194 if not opt.isdigit() or not (MIN_PHOTO_DIMENSION <= int(opt) <= MAX_PHOTO_DIMENSION): 195 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) 196 # VALIDATION ENDS ######################################### 197 kwargs[option] = True 198 kwargs['photo_options'] = args 199 elif option in ('ratings_optional', 'ratings_required') and not self.free: 200 # VALIDATION ############################################## 201 if 2 < len(args.split('|')) > 9: 202 raise template.TemplateSyntaxError, "Incorrect number of '%s' options in %r tag. Use between 2 and 8." % (option, tokens[0]) 203 if re.match('^scale:\d+\-\d+\:$', args.split('|')[0]): 204 raise template.TemplateSyntaxError, "Invalid 'scale' in %r tag's '%s' options" % (tokens[0], option) 205 # VALIDATION ENDS ######################################### 206 kwargs[option] = True 207 kwargs['rating_options'] = args 208 elif option in ('is_public'): 209 kwargs[option] = (args == 'true') 210 else: 211 raise template.TemplateSyntaxError, "%r tag got invalid parameter '%s'" % (tokens[0], option) 212 return CommentFormNode(content_type, obj_id_lookup_var, obj_id, self.free, **kwargs) 213 214 class DoCommentCount: 215 """ 216 Gets comment count for the given params and populates the template context 217 with a variable containing that value, whose name is defined by the 'as' 218 clause. 219 220 Syntax:: 221 222 {% get_comment_count for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname] %} 182 {% get_comment_count for event as comment_count %} 183 {% get_comment_count for calendar.event event.id as comment_count %} 184 {% get_comment_count for calendar.event 17 as comment_count %} 185 186 """ 187 return CommentCountNode.handle_token(parser, token) 188 189 #@register.tag 190 def get_comment_list(parser, token): 191 """ 192 Gets the list of comments for the given params and populates the template 193 context with a variable containing that value, whose name is defined by the 194 'as' clause. 195 196 Syntax:: 197 198 {% get_comment_list for [object] as [varname] %} 199 {% get_comment_list for [app].[model] [object_id] as [varname] %} 223 200 224 201 Example usage:: 225 202 226 {% get_comment_count for lcom.eventtimes event.id as comment_count %} 227 228 Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this:: 229 230 {% get_comment_count for lcom.eventtimes 23 as comment_count %} 231 """ 232 def __init__(self, free): 233 self.free = free 234 235 def __call__(self, parser, token): 236 tokens = token.contents.split() 237 # Now tokens is a list like this: 238 # ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list'] 239 if len(tokens) != 6: 240 raise template.TemplateSyntaxError, "%r tag requires 5 arguments" % tokens[0] 241 if tokens[1] != 'for': 242 raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0] 243 try: 244 package, module = tokens[2].split('.') 245 except ValueError: # unpack list of wrong size 246 raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] 247 try: 248 content_type = ContentType.objects.get(app_label__exact=package, model__exact=module) 249 except ContentType.DoesNotExist: 250 raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) 251 var_name, obj_id = None, None 252 if tokens[3].isdigit(): 253 obj_id = tokens[3] 254 try: # ensure the object ID is valid 255 content_type.get_object_for_this_type(pk=obj_id) 256 except ObjectDoesNotExist: 257 raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id) 258 else: 259 var_name = tokens[3] 260 if tokens[4] != 'as': 261 raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0] 262 return CommentCountNode(package, module, var_name, obj_id, tokens[5], self.free) 263 264 class DoGetCommentList: 265 """ 266 Gets comments for the given params and populates the template context with a 267 special comment_package variable, whose name is defined by the ``as`` 268 clause. 269 270 Syntax:: 271 272 {% get_comment_list for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname] (reversed) %} 273 274 Example usage:: 275 276 {% get_comment_list for lcom.eventtimes event.id as comment_list %} 277 278 Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this:: 279 280 {% get_comment_list for lcom.eventtimes 23 as comment_list %} 281 282 To get a list of comments in reverse order -- that is, most recent first -- 283 pass ``reversed`` as the last param:: 284 285 {% get_comment_list for lcom.eventtimes event.id as comment_list reversed %} 286 """ 287 def __init__(self, free): 288 self.free = free 289 290 def __call__(self, parser, token): 291 tokens = token.contents.split() 292 # Now tokens is a list like this: 293 # ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list'] 294 if not len(tokens) in (6, 7): 295 raise template.TemplateSyntaxError, "%r tag requires 5 or 6 arguments" % tokens[0] 296 if tokens[1] != 'for': 297 raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0] 298 try: 299 package, module = tokens[2].split('.') 300 except ValueError: # unpack list of wrong size 301 raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] 302 try: 303 content_type = ContentType.objects.get(app_label__exact=package,model__exact=module) 304 except ContentType.DoesNotExist: 305 raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) 306 var_name, obj_id = None, None 307 if tokens[3].isdigit(): 308 obj_id = tokens[3] 309 try: # ensure the object ID is valid 310 content_type.get_object_for_this_type(pk=obj_id) 311 except ObjectDoesNotExist: 312 raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id) 313 else: 314 var_name = tokens[3] 315 if tokens[4] != 'as': 316 raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0] 317 if len(tokens) == 7: 318 if tokens[6] != 'reversed': 319 raise template.TemplateSyntaxError, "Final argument in %r must be 'reversed' if given" % tokens[0] 320 ordering = "-" 321 else: 322 ordering = "" 323 return CommentListNode(package, module, var_name, obj_id, tokens[5], self.free, ordering) 324 325 # registration comments 326 register.tag('get_comment_list', DoGetCommentList(False)) 327 register.tag('comment_form', DoCommentForm(False)) 328 register.tag('get_comment_count', DoCommentCount(False)) 329 # free comments 330 register.tag('get_free_comment_list', DoGetCommentList(True)) 331 register.tag('free_comment_form', DoCommentForm(True)) 332 register.tag('get_free_comment_count', DoCommentCount(True)) 203 {% get_comment_list for event as comment_list %} 204 {% for comment in comment_list %} 205 ... 206 {% endfor %} 207 208 """ 209 return CommentListNode.handle_token(parser, token) 210 211 #@register.tag 212 def get_comment_form(parser, token): 213 """ 214 Get a (new) form object to post a new comment. 215 216 Syntax:: 217 218 {% get_comment_form for [object] as [varname] %} 219 {% get_comment_form for [app].[model] [object_id] as [varname] %} 220 """ 221 return CommentFormNode.handle_token(parser, token) 222 223 #@register.tag 224 def render_comment_form(parser, token): 225 """ 226 Render the comment form (as returned by ``{% render_comment_form %}``) through 227 the ``comments/form.html`` template. 228 229 Syntax:: 230 231 {% render_comment_form for [object] %} 232 {% render_comment_form for [app].[model] [object_id] %} 233 """ 234 return RenderCommentFormNode.handle_token(parser, token) 235 236 #@register.simple_tag 237 def comment_form_target(): 238 """ 239 Get the target URL for the comment form. 240 241 Example:: 242 243 <form action="{% comment_form_target %}" method="POST"> 244 """ 245 return comments.get_form_target() 246 247 register.tag(get_comment_count) 248 register.tag(get_comment_list) 249 register.tag(get_comment_form) 250 register.tag(render_comment_form) 251 register.simple_tag(comment_form_target) django/trunk/django/contrib/comments/views/comments.py
r7967 r8557 1 import base64 2 import datetime 3 4 from django.core import validators 5 from django import oldforms 6 from django.core.mail import mail_admins, mail_managers 7 from django.http import Http404 1 from django import http 2 from django.conf import settings 3 from utils import next_redirect, confirmation_view 8 4 from django.core.exceptions import ObjectDoesNotExist 5 from django.db import models 9 6 from django.shortcuts import render_to_response 10 7 from django.template import RequestContext 11 from django.contrib.comments.models import Comment, FreeComment, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC 12 from django.contrib.contenttypes.models import ContentType 13 from django.contrib.auth import authenticate 14 from django.http import HttpResponseRedirect 15 from django.utils.text import normalize_newlines 16 from django.conf import settings 17 from django.utils.translation import ungettext, ugettext as _ 18 from django.utils.encoding import smart_unicode 8 from django.template.loader import render_to_string 9 from django.utils.html import escape 10 from django.contrib import comments 11 from django.contrib.comments import signals 19 12 20 COMMENTS_PER_PAGE = 20 13 class CommentPostBadRequest(http.HttpResponseBadRequest): 14 """ 15 Response returned when a comment post is invalid. If ``DEBUG`` is on a 16 nice-ish error message will be displayed (for debugging purposes), but in 17 production mode a simple opaque 400 page will be displayed. 18 """ 19 def __init__(self, why): 20 super(CommentPostBadRequest, self).__init__() 21 if settings.DEBUG: 22 self.content = render_to_string("comments/400-debug.html", {"why": why}) 21 23 22 # TODO: This is a copy of the manipulator-based form that used to live in 23 # contrib.auth.forms. It should be replaced with the newforms version that 24 # has now been added to contrib.auth.forms when the comments app gets updated 25 # for newforms. 24 def post_comment(request, next=None): 25 """ 26 Post a comment. 26 27 27 class AuthenticationForm(oldforms.Manipulator): 28 HTTP POST is required. If ``POST['submit'] == "preview"`` or if there are 29 errors a preview template, ``comments/preview.html``, will be rendered. 28 30 """ 29 Base class for authenticating users. Extend this to get a form that accepts 30 username/password logins. 31 """ 32 def __init__(self, request=None): 33 """ 34 If request is passed in, the manipulator will validate that cookies are 35 enabled. Note that the request (a HttpRequest object) must have set a 36 cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before 37 running this validator. 38 """ 39 self.request = request 40 self.fields = [ 41 oldforms.TextField(field_name="username", length=15, max_length=30, is_required=True, 42 validator_list=[self.isValidUser, self.hasCookiesEnabled]), 43 oldforms.PasswordField(field_name="password", length=15, max_length=30, is_required=True), 31 32 # Require POST 33 if request.method != 'POST': 34 return http.HttpResponseNotAllowed(["POST"]) 35 36 # Fill out some initial data fields from an authenticated user, if present 37 data = request.POST.copy() 38 if request.user.is_authenticated(): 39 if "name" not in data: 40 data["name"] = request.user.get_full_name() 41 if "email" not in data: 42 data["email"] = request.user.email 43 44 # Look up the object we're trying to comment about 45 ctype = data.get("content_type") 46 object_pk = data.get("object_pk") 47 if ctype is None or object_pk is None: 48 return CommentPostBadRequest("Missing content_type or object_pk field.") 49 try: 50 model = models.get_model(*ctype.split(".", 1)) 51 target = model._default_manager.get(pk=object_pk) 52 except TypeError: 53 return CommentPostBadRequest( 54 "Invalid content_type value: %r" % escape(ctype)) 55 except AttributeError: 56 return CommentPostBadRequest( 57 "The given content-type %r does not resolve to a valid model." % \ 58 escape(ctype)) 59 except ObjectDoesNotExist: 60 return CommentPostBadRequest( 61 "No object matching content-type %r and object PK %r exists." % \ 62 (escape(ctype), escape(object_pk))) 63 64 # Do we want to preview the comment? 65 preview = data.get("submit", "").lower() == "preview" or \ 66 data.get("preview", None) is not None 67 68 # Construct the comment form 69 form = comments.get_form()(target, data=data) 70 71 # Check security information 72 if form.security_errors(): 73 return CommentPostBadRequest( 74 "The comment form failed security verification: %s" % \ 75 escape(str(form.security_errors()))) 76 77 # If there are errors or if we requested a preview show the comment 78 if form.errors or preview: 79 template_list = [ 80 "comments/%s_%s_preview.html" % tuple(str(model._meta).split(".")), 81 "comments/%s_preview.html" % model._meta.app_label, 82 "comments/preview.html", 44 83 ] 45 self.user_cache = None 46 47 def hasCookiesEnabled(self, field_data, all_data): 48 if self.request and not self.request.session.test_cookie_worked(): 49 raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.") 50 51 def isValidUser(self, field_data, all_data): 52 username = field_data 53 password = all_data.get('password', None) 54 self.user_cache = authenticate(username=username, password=password) 55 if self.user_cache is None: 56 raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.") 57 elif not self.user_cache.is_active: 58 raise validators.ValidationError, _("This account is inactive.") 59 60 def get_user_id(self): 61 if self.user_cache: 62 return self.user_cache.id 63 return None 64 65 def get_user(self): 66 return self.user_cache 67 68 class PublicCommentManipulator(AuthenticationForm): 69 "Manipulator that handles public registered comments" 70 def __init__(self, user, ratings_required, ratings_range, num_rating_choices): 71 AuthenticationForm.__init__(self) 72 self.ratings_range, self.num_rating_choices = ratings_range, num_rating_choices 73 choices = [(c, c) for c in ratings_range] 74 def get_validator_list(rating_num): 75 if rating_num <= num_rating_choices: 76 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."))] 77 else: 78 return [] 79 self.fields.extend([ 80 oldforms.LargeTextField(field_name="comment", max_length=3000, is_required=True, 81 validator_list=[self.hasNoProfanities]), 82 oldforms.RadioSelectField(field_name="rating1", choices=choices, 83 is_required=ratings_required and num_rating_choices > 0, 84 validator_list=get_validator_list(1), 85 ), 86 oldforms.RadioSelectField(field_name="rating2", choices=choices, 87 is_required=ratings_required and num_rating_choices > 1, 88 validator_list=get_validator_list(2), 89 ), 90 oldforms.RadioSelectField(field_name="rating3", choices=choices, 91 is_required=ratings_required and num_rating_choices > 2, 92 validator_list=get_validator_list(3), 93 ), 94 oldforms.RadioSelectField(field_name="rating4", choices=choices, 95 is_required=ratings_required and num_rating_choices > 3, 96 validator_list=get_validator_list(4), 97 ), 98 oldforms.RadioSelectField(field_name="rating5", choices=choices, 99 is_required=ratings_required and num_rating_choices > 4, 100 validator_list=get_validator_list(5), 101 ), 102 oldforms.RadioSelectField(field_name="rating6", choices=choices, 103 is_required=ratings_required and num_rating_choices > 5, 104 validator_list=get_validator_list(6), 105 ), 106 oldforms.RadioSelectField(field_name="rating7", choices=choices, 107 is_required=ratings_required and num_rating_choices > 6, 108 validator_list=get_validator_list(7), 109 ), 110 oldforms.RadioSelectField(field_name="rating8", choices=choices, 111 is_required=ratings_required and num_rating_choices > 7, 112 validator_list=get_validator_list(8), 113 ), 114 ]) 115 if user.is_authenticated(): 116 self["username"].is_required = False 117 self["username"].validator_list = [] 118 self["password"].is_required = False 119 self["password"].validator_list = [] 120 self.user_cache = user 121 122 def hasNoProfanities(self, field_data, all_data): 123 if settings.COMMENTS_ALLOW_PROFANITIES: 124 return 125 return validators.hasNoProfanities(field_data, all_data) 126 127 def get_comment(self, new_data): 128 "Helper function" 129 return Comment(None, self.get_user_id(), new_data["content_type_id"], 130 new_data["object_id"], new_data.get("headline", "").strip(), 131 new_data["comment"].strip(), new_data.get("rating1", None), 132 new_data.get("rating2", None), new_data.get("rating3", None), 133 new_data.get("rating4", None), new_data.get("rating5", None), 134 new_data.get("rating6", None), new_data.get("rating7", None), 135 new_data.get("rating8", None), new_data.get("rating1", None) is not None, 136 datetime.datetime.now(), new_data["is_public"], new_data["ip_address"], False, settings.SITE_ID) 137 138 def save(self, new_data): 139 today = datetime.date.today() 140 c = self.get_comment(new_data) 141 for old in Comment.objects.filter(content_type__id__exact=new_data["content_type_id"], 142 object_id__exact=new_data["object_id"], user__id__exact=self.get_user_id()): 143 # Check that this comment isn't duplicate. (Sometimes people post 144 # comments twice by mistake.) If it is, fail silently by pretending 145 # the comment was posted successfully. 146 if old.submit_date.date() == today and old.comment == c.comment \ 147 and old.rating1 == c.rating1 and old.rating2 == c.rating2 \ 148 and old.rating3 == c.rating3 and old.rating4 == c.rating4 \ 149 and old.rating5 == c.rating5 and old.rating6 == c.rating6 \ 150 and old.rating7 == c.rating7 and old.rating8 == c.rating8: 151 return old 152 # If the user is leaving a rating, invalidate all old ratings. 153 if c.rating1 is not None: 154 old.valid_rating = False 155 old.save() 156 c.save() 157 # If the commentor has posted fewer than COMMENTS_FIRST_FEW comments, 158 # send the comment to the managers. 159 if self.user_cache.comment_set.count() <= settings.COMMENTS_FIRST_FEW: 160 message = ungettext('This comment was posted by a user who has posted fewer than %(count)s comment:\n\n%(text)s', 161 'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s', settings.COMMENTS_FIRST_FEW) % \ 162 {'count': settings.COMMENTS_FIRST_FEW, 'text': c.get_as_text()} 163 mail_managers("Comment posted by rookie user", message) 164 if settings.COMMENTS_SKETCHY_USERS_GROUP and settings.COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.groups.all()]: 165 message = _('This comment was posted by a sketchy user:\n\n%(text)s') % {'text': c.get_as_text()} 166 mail_managers("Comment posted by sketchy user (%s)" % self.user_cache.username, c.get_as_text()) 167 return c 168 169 class PublicFreeCommentManipulator(oldforms.Manipulator): 170 "Manipulator that handles public free (unregistered) comments" 171 def __init__(self): 172 self.fields = ( 173 oldforms.TextField(field_name="person_name", max_length=50, is_required=True, 174 validator_list=[self.hasNoProfanities]), 175 oldforms.LargeTextField(field_name="comment", max_length=3000, is_required=True, 176 validator_list=[self.hasNoProfanities]), 84 return render_to_response( 85 template_list, { 86 "comment" : form.data.get("comment", ""), 87 "form" : form, 88 }, 89 RequestContext(request, {}) 177 90 ) 178 91 179 def hasNoProfanities(self, field_data, all_data): 180 if settings.COMMENTS_ALLOW_PROFANITIES: 181 return 182 return validators.hasNoProfanities(field_data, all_data) 92 # Otherwise create the comment 93 comment = form.get_comment_object() 94 comment.ip_address = request.META.get("REMOTE_ADDR", None) 95 if request.user.is_authenticated(): 96 comment.user = request.user 183 97 184 def get_comment(self, new_data): 185 "Helper function" 186 return FreeComment(None, new_data["content_type_id"], 187 new_data["object_id"], new_data["comment"].strip(), 188 new_data["person_name"].strip(), datetime.datetime.now(), new_data["is_public"], 189 new_data["ip_address"], False, settings.SITE_ID) 98 # Signal that the comment is about to be saved 99 responses = signals.comment_will_be_posted.send(comment) 190 100 191 def save(self, new_data): 192 today = datetime.date.today() 193 c = self.get_comment(new_data) 194 # Check that this comment isn't duplicate. (Sometimes people post 195 # comments twice by mistake.) If it is, fail silently by pretending 196 # the comment was posted successfully. 197 for old_comment in FreeComment.objects.filter(content_type__id__exact=new_data["content_type_id"], 198 object_id__exact=new_data["object_id"], person_name__exact=new_data["person_name"], 199 submit_date__year=today.year, submit_date__month=today.month, 200 submit_date__day=today.day): 201 if old_comment.comment == c.comment: 202 return old_comment 203 c.save() 204 return c 101 for (receiver, response) in responses: 102 if response == False: 103 return CommentPostBadRequest( 104 "comment_will_be_posted receiver %r killed the comment" % receiver.__name__) 205 105 206 def post_comment(request, extra_context=None, context_processors=None): 207 """208 Post a comment106 # Save the comment and signal that it was saved 107 comment.save() 108 signals.comment_was_posted.send(comment) 209 109 210 Redirects to the `comments.comments.comment_was_posted` view upon success.110 return next_redirect(data, next, comment_done, c=comment._get_pk_val()) 211 111 212 Templates: `comment_preview` 213 Context: 214 comment 215 the comment being posted 216 comment_form 217 the comment form 218 options 219 comment options 220 target 221 comment target 222 hash 223 security hash (must be included in a posted form to succesfully 224 post a comment). 225 rating_options 226 comment ratings options 227 ratings_optional 228 are ratings optional? 229 ratings_required 230 are ratings required? 231 rating_range 232 range of ratings 233 rating_choices 234 choice of ratings 235 """ 236 if extra_context is None: extra_context = {} 237 if not request.POST: 238 raise Http404, _("Only POSTs are allowed") 239 try: 240 options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo'] 241 except KeyError: 242 raise Http404, _("One or more of the required fields wasn't submitted") 243 photo_options = request.POST.get('photo_options', '') 244 rating_options = normalize_newlines(request.POST.get('rating_options', '')) 245 if Comment.objects.get_security_hash(options, photo_options, rating_options, target) != security_hash: 246 raise Http404, _("Somebody tampered with the comment form (security violation)") 247 # Now we can be assured the data is valid. 248 if rating_options: 249 rating_range, rating_choices = Comment.objects.get_rating_options(base64.decodestring(rating_options)) 250 else: 251 rating_range, rating_choices = [], [] 252 content_type_id, object_id = target.split(':') # target is something like '52:5157' 253 try: 254 obj = ContentType.objects.get(pk=content_type_id).get_object_for_this_type(pk=object_id) 255 except ObjectDoesNotExist: 256 raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid") 257 option_list = options.split(',') # options is something like 'pa,ra' 258 new_data = request.POST.copy() 259 new_data['content_type_id'] = content_type_id 260 new_data['object_id'] = object_id 261 new_data['ip_address'] = request.META.get('REMOTE_ADDR') 262 new_data['is_public'] = IS_PUBLIC in option_list 263 manipulator = PublicCommentManipulator(request.user, 264 ratings_required=RATINGS_REQUIRED in option_list, 265 ratings_range=rating_range, 266 num_rating_choices=len(rating_choices)) 267 errors = manipulator.get_validation_errors(new_data) 268 # If user gave correct username/password and wasn't already logged in, log them in 269 # so they don't have to enter a username/password again. 270 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']): 271 from django.contrib.auth import login 272 login(request, manipulator.get_user()) 273 if errors or 'preview' in request.POST: 274 class CommentFormWrapper(oldforms.FormWrapper): 275 def __init__(self, manipulator, new_data, errors, rating_choices): 276 oldforms.FormWrapper.__init__(self, manipulator, new_data, errors) 277 self.rating_choices = rating_choices 278 def ratings(self): 279 field_list = [self['rating%d' % (i+1)] for i in range(len(rating_choices))] 280 for i, f in enumerate(field_list): 281 f.choice = rating_choices[i] 282 return field_list 283 comment = errors and '' or manipulator.get_comment(new_data) 284 comment_form = CommentFormWrapper(manipulator, new_data, errors, rating_choices) 285 return render_to_response('comments/preview.html', { 286 'comment': comment, 287 'comment_form': comment_form, 288 'options': options, 289 'target': target, 290 'hash': security_hash, 291 'rating_options': rating_options, 292 'ratings_optional': RATINGS_OPTIONAL in option_list, 293 'ratings_required': RATINGS_REQUIRED in option_list, 294 'rating_range': rating_range, 295 'rating_choices': rating_choices, 296 }, context_instance=RequestContext(request, extra_context, context_processors)) 297 elif 'post' in request.POST: 298 # If the IP is banned, mail the admins, do NOT save the comment, and 299 # serve up the "Thanks for posting" page as if the comment WAS posted. 300 if request.META['REMOTE_ADDR'] in settings.BANNED_IPS: 301 mail_admins("Banned IP attempted to post comment", smart_unicode(request.POST) + "\n\n" + str(request.META)) 302 else: 303 manipulator.do_html2python(new_data) 304 comment = manipulator.save(new_data) 305 return HttpResponseRedirect("../posted/?c=%s:%s" % (content_type_id, object_id)) 306 else: 307 raise Http404, _("The comment form didn't provide either 'preview' or 'post'") 112 comment_done = confirmation_view( 113 template = "comments/posted.html", 114 doc = """Display a "comment was posted" success page.""" 115 ) 308 116 309 def post_free_comment(request, extra_context=None, context_processors=None):310 """311 Post a free comment (not requiring a log in)312 313 Redirects to `comments.comments.comment_was_posted` view on success.314 315 Templates: `comment_free_preview`316 Context:317 comment318 comment being posted319 comment_form320 comment form object321 options322 comment options323 target324 comment target325 hash326 security hash (must be included in a posted form to succesfully327 post a comment).328 """329 if extra_context is None: extra_context = {}330 if not request.POST:331 raise Http404, _("Only POSTs are allowed")332 try:333 options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo']334 except KeyError:335 raise Http404, _("One or more of the required fields wasn't submitted")336 if Comment.objects.get_security_hash(options, '', '', target) != security_hash:337 raise Http404, _("Somebody tampered with the comment form (security violation)")338 content_type_id, object_id = target.split(':') # target is something like '52:5157'339 content_type = ContentType.objects.get(pk=content_type_id)340 try:341 obj = content_type.get_object_for_this_type(pk=object_id)342 except ObjectDoesNotExist:343 raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid")344 option_list = options.split(',')345 new_data = request.POST.copy()346 new_data['content_type_id'] = content_type_id347 new_data['object_id'] = object_id348 new_data['ip_address'] = request.META['REMOTE_ADDR']349 new_data['is_public'] = IS_PUBLIC in option_list350 manipulator = PublicFreeCommentManipulator()351 errors = manipulator.get_validation_errors(new_data)352 if errors or 'preview' in request.POST:353 comment = errors and '' or manipulator.get_comment(new_data)354 return render_to_response('comments/free_preview.html', {355 'comment': comment,356 'comment_form': oldforms.FormWrapper(manipulator, new_data, errors),357 'options': options,358 'target': target,359 'hash': security_hash,360 }, context_instance=RequestContext(request, extra_context, context_processors))361 elif 'post' in request.POST:362 # If the IP is banned, mail the admins, do NOT save the comment, and363 # serve up the "Thanks for posting" page as if the comment WAS posted.364 if request.META['REMOTE_ADDR'] in settings.BANNED_IPS:365 from django.core.mail import mail_admins366 mail_admins("Practical joker", smart_unicode(request.POST) + "\n\n" + str(request.META))367 else:368 manipulator.do_html2python(new_data)369 comment = manipulator.save(new_data)370 return HttpResponseRedirect("../posted/?c=%s:%s" % (content_type_id, object_id))371 else:372 raise Http404, _("The comment form didn't provide either 'preview' or 'post'")373 374 def comment_was_posted(request, extra_context=None, context_processors=None):375 """376 Display "comment was posted" success page377 378 Templates: `comment_posted`379 Context:380 object381 The object the comment was posted on382 """383 if extra_context is None: extra_context = {}384 obj = None385 if 'c' in request.GET:386 content_type_id, object_id = request.GET['c'].split(':')387 try:388 content_type = ContentType.objects.get(pk=content_type_id)389 obj = content_type.get_object_for_this_type(pk=object_id)390 except ObjectDoesNotExist:391 pass392 return render_to_response('comments/posted.html', {'object': obj},393 context_instance=RequestContext(request, extra_context, context_processors))django/trunk/docs/index.txt
r8506 r8557 73 73 --------- 74 74 75 :ref:`topics-auth` ... :ref:`topics-cache` ... :ref:`topics-email` ... 76 :ref:`topics-files` ... :ref:`topics-i18n` ... :ref:`topics-install` ... 77 :ref:`topics-pagination` ... :ref:`topics-serialization` ... 78 :ref:`topics-settings` ... :ref:`topics-testing` 75 * :ref:`topics-auth` 76 * :ref:`topics-cache` 77 * :ref:`topics-email` 78 * :ref:`topics-files` 79 * :ref:`topics-i18n` 80 * :ref:`topics-install` 81 * :ref:`topics-pagination` 82 * :ref:`topics-serialization` 83 * :ref:`topics-settings` 84 * :ref:`topics-testing` 79 85 80 86 Add-on ("contrib") applications … … 96 102 --------- 97 103 98 :ref:`ref-contrib-contenttypes` ... :ref:`ref-contrib-csrf` ... 99 :ref:`ref-contrib-databrowse` ... :ref:`ref-contrib-flatpages` ... 100 :ref:`ref-contrib-humanize` ... :ref:`ref-contrib-redirects` ... 101 :ref:`ref-contrib-sitemaps` ... :ref:`ref-contrib-sites` ... 102 :ref:`ref-contrib-webdesign` 104 * :ref:`ref-contrib-comments-index` 105 * :ref:`ref-contrib-contenttypes` 106 * :ref:`ref-contrib-csrf` 107 * :ref:`ref-contrib-databrowse` 108 * :ref:`ref-contrib-flatpages` 109 * :ref:`ref-contrib-humanize` 110 * :ref:`ref-contrib-redirects` 111 * :ref:`ref-contrib-sitemaps` 112 * :ref:`ref-contrib-sites` 113 * :ref:`ref-contrib-webdesign` 103 114 104 115 Solving specific problems … … 121 132 --------- 122 133 123 :ref:`Authenticating in Apache <howto-apache-auth>` ... 124 :ref:`howto-custom-file-storage` ... :ref:`howto-custom-management-commands` ... 125 :ref:`howto-custom-model-fields` ... :ref:`howto-error-reporting` ... 126 :ref:`howto-initial-data` ... :ref:`howto-static-files` 127 134 * :ref:`Authenticating in Apache <howto-apache-auth>` 135 * :ref:`howto-custom-file-storage` 136 * :ref:`howto-custom-management-commands` 137 * :ref:`howto-custom-model-fields` 138 * :ref:`howto-error-reporting` 139 * :ref:`howto-initial-data` 140 * :ref:`howto-static-files` 141 128 142 Reference 129 143 ========= … … 144 158 --------- 145 159 146 :ref:`ref-databases` ... :ref:`ref-django-admin` ... :ref:`ref-files-index` ... 147 :ref:`ref-generic-views` ... :ref:`ref-middleware` ... 148 :ref:`ref-templates-index` ... :ref:`ref-unicode` 160 * :ref:`ref-databases` 161 * :ref:`ref-django-admin` 162 * :ref:`ref-files-index` 163 * :ref:`ref-generic-views` 164 * :ref:`ref-middleware` 165 * :ref:`ref-templates-index` 166 * :ref:`ref-unicode` 149 167 150 168 And all the rest django/trunk/docs/ref/contrib/index.txt
r8506 r8557 27 27 admin 28 28 auth 29 comments/index 29 30 contenttypes 30 31 csrf … … 59 60 ======== 60 61 61 A simple yet flexible comments system. This is not yet documented. 62 **New in Django development version.** 63 64 A simple yet flexible comments system. See :ref:`ref-contrib-comments-index`. 62 65 63 66 contenttypes django/trunk/docs/_static/djangodocs.css
r8506 r8557 63 63 ul { padding-left:30px; } 64 64 ol { padding-left:30px; } 65 ol.arabic { list-style-type: decimal; }65 ol.arabic li { list-style-type: decimal; } 66 66 ul li { list-style-type:square; margin-bottom:.4em; } 67 67 ol li { margin-bottom: .4em; } django/trunk/docs/topics/templates.txt
r8506 r8557 608 608 variable and object available to you in a given template. 609 609 610 .. _loading-custom-template-libraries: 611 610 612 Custom tag and filter libraries 611 613 ===============================
