Changeset 8557
- Timestamp:
- 08/25/08 17:14:22 (3 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', PHO
