app/django/contrib/comments/models.py
changeset 323 ff1a9aa48cfd
parent 54 03e267d67478
equal deleted inserted replaced
322:6641e941ef1e 323:ff1a9aa48cfd
     1 import datetime
     1 import datetime
     2 
     2 from django.contrib.auth.models import User
     3 from django.db import models
     3 from django.contrib.comments.managers import CommentManager
       
     4 from django.contrib.contenttypes import generic
     4 from django.contrib.contenttypes.models import ContentType
     5 from django.contrib.contenttypes.models import ContentType
     5 from django.contrib.sites.models import Site
     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
     7 from django.utils.translation import ugettext_lazy as _
     9 from django.utils.translation import ugettext_lazy as _
     8 from django.conf import settings
    10 from django.conf import settings
     9 
    11 
    10 MIN_PHOTO_DIMENSION = 5
    12 COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000)
    11 MAX_PHOTO_DIMENSION = 1000
       
    12 
    13 
    13 # Option codes for comment-form hidden fields.
    14 class BaseCommentAbstractModel(models.Model):
    14 PHOTOS_REQUIRED = 'pr'
    15     """
    15 PHOTOS_OPTIONAL = 'pa'
    16     An abstract base class that any custom comment models probably should
    16 RATINGS_REQUIRED = 'rr'
    17     subclass.
    17 RATINGS_OPTIONAL = 'ra'
    18     """
    18 IS_PUBLIC = 'ip'
       
    19 
    19 
    20 # What users get if they don't have any karma.
    20     # Content-object field
    21 DEFAULT_KARMA = 5
    21     content_type   = models.ForeignKey(ContentType,
    22 KARMA_NEEDED_BEFORE_DISPLAYED = 3
    22             related_name="content_type_set_for_%(class)s")
       
    23     object_pk      = models.TextField(_('object ID'))
       
    24     content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
    23 
    25 
       
    26     # Metadata about the comment
       
    27     site        = models.ForeignKey(Site)
    24 
    28 
    25 class CommentManager(models.Manager):
    29     class Meta:
    26     def get_security_hash(self, options, photo_options, rating_options, target):
    30         abstract = True
       
    31 
       
    32     def get_content_object_url(self):
    27         """
    33         """
    28         Returns the MD5 hash of the given options (a comma-separated string such as
    34         Get a URL suitable for redirecting to the content object.
    29         'pa,ra') and target (something like 'lcom.eventtimes:5157'). Used to
       
    30         validate that submitted form options have not been tampered-with.
       
    31         """
    35         """
    32         import md5
    36         return urlresolvers.reverse(
    33         return md5.new(options + photo_options + rating_options + target + settings.SECRET_KEY).hexdigest()
    37             "comments-url-redirect",
       
    38             args=(self.content_type_id, self.object_pk)
       
    39         )
    34 
    40 
    35     def get_rating_options(self, rating_string):
    41 class Comment(BaseCommentAbstractModel):
    36         """
    42     """
    37         Given a rating_string, this returns a tuple of (rating_range, options).
    43     A user comment about some object.
    38         >>> s = "scale:1-10|First_category|Second_category"
    44     """
    39         >>> Comment.objects.get_rating_options(s)
       
    40         ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category'])
       
    41         """
       
    42         rating_range, options = rating_string.split('|', 1)
       
    43         rating_range = range(int(rating_range[6:].split('-')[0]), int(rating_range[6:].split('-')[1])+1)
       
    44         choices = [c.replace('_', ' ') for c in options.split('|')]
       
    45         return rating_range, choices
       
    46 
    45 
    47     def get_list_with_karma(self, **kwargs):
    46     # Who posted this comment? If ``user`` is set then it was an authenticated
    48         """
    47     # user; otherwise at least user_name should have been set and the comment
    49         Returns a list of Comment objects matching the given lookup terms, with
    48     # was posted by a non-authenticated user.
    50         _karma_total_good and _karma_total_bad filled.
    49     user        = models.ForeignKey(User, blank=True, null=True, related_name="%(class)s_comments")
    51         """
    50     user_name   = models.CharField(_("user's name"), max_length=50, blank=True)
    52         extra_kwargs = {}
    51     user_email  = models.EmailField(_("user's email address"), blank=True)
    53         extra_kwargs.setdefault('select', {})
    52     user_url    = models.URLField(_("user's URL"), blank=True)
    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)
       
    57 
    53 
    58     def user_is_moderator(self, user):
    54     comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
    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
       
    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):
    67     # Manager
    68     """A comment by a registered user."""
       
    69     user = models.ForeignKey(User, raw_id_admin=True)
       
    70     content_type = models.ForeignKey(ContentType)
       
    71     object_id = models.IntegerField(_('object ID'))
       
    72     headline = models.CharField(_('headline'), max_length=255, blank=True)
       
    73     comment = models.TextField(_('comment'), max_length=3000)
       
    74     rating1 = models.PositiveSmallIntegerField(_('rating #1'), blank=True, null=True)
       
    75     rating2 = models.PositiveSmallIntegerField(_('rating #2'), blank=True, null=True)
       
    76     rating3 = models.PositiveSmallIntegerField(_('rating #3'), blank=True, null=True)
       
    77     rating4 = models.PositiveSmallIntegerField(_('rating #4'), blank=True, null=True)
       
    78     rating5 = models.PositiveSmallIntegerField(_('rating #5'), blank=True, null=True)
       
    79     rating6 = models.PositiveSmallIntegerField(_('rating #6'), blank=True, null=True)
       
    80     rating7 = models.PositiveSmallIntegerField(_('rating #7'), blank=True, null=True)
       
    81     rating8 = models.PositiveSmallIntegerField(_('rating #8'), blank=True, null=True)
       
    82     # This field designates whether to use this row's ratings in aggregate
       
    83     # functions (summaries). We need this because people are allowed to post
       
    84     # multiple reviews on the same thing, but the system will only use the
       
    85     # latest one (with valid_rating=True) in tallying the reviews.
       
    86     valid_rating = models.BooleanField(_('is valid rating'))
       
    87     submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True)
       
    88     is_public = models.BooleanField(_('is public'))
       
    89     ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
       
    90     is_removed = models.BooleanField(_('is removed'), help_text=_('Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.'))
       
    91     site = models.ForeignKey(Site)
       
    92     objects = CommentManager()
    68     objects = CommentManager()
    93 
    69 
    94     class Meta:
    70     class Meta:
    95         verbose_name = _('comment')
    71         db_table = "django_comments"
    96         verbose_name_plural = _('comments')
    72         ordering = ('submit_date',)
    97         ordering = ('-submit_date',)
    73         permissions = [("can_moderate", "Can moderate comments")]
    98 
       
    99     class Admin:
       
   100         fields = (
       
   101             (None, {'fields': ('content_type', 'object_id', 'site')}),
       
   102             ('Content', {'fields': ('user', 'headline', 'comment')}),
       
   103             ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}),
       
   104             ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}),
       
   105         )
       
   106         list_display = ('user', 'submit_date', 'content_type', 'get_content_object')
       
   107         list_filter = ('submit_date',)
       
   108         date_hierarchy = 'submit_date'
       
   109         search_fields = ('comment', 'user__username')
       
   110 
    74 
   111     def __unicode__(self):
    75     def __unicode__(self):
   112         return "%s: %s..." % (self.user.username, self.comment[:100])
    76         return "%s: %s..." % (self.name, self.comment[:50])
   113 
    77 
   114     def get_absolute_url(self):
    78     def save(self, force_insert=False, force_update=False):
   115         try:
    79         if self.submit_date is None:
   116             return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
    80             self.submit_date = datetime.datetime.now()
   117         except AttributeError:
    81         super(Comment, self).save(force_insert, force_update)
   118             return ""
       
   119 
    82 
   120     def get_crossdomain_url(self):
    83     def _get_userinfo(self):
   121         return "/r/%d/%d/" % (self.content_type_id, self.object_id)
    84         """
       
    85         Get a dictionary that pulls together information about the poster
       
    86         safely for both authenticated and non-authenticated comments.
   122 
    87 
   123     def get_flag_url(self):
    88         This dict will have ``name``, ``email``, and ``url`` fields.
   124         return "/comments/flag/%s/" % self.id
    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
   125 
   100 
   126     def get_deletion_url(self):
   101                 # If the user has a full name, use that for the user name.
   127         return "/comments/delete/%s/" % self.id
   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__)
   128 
   110 
   129     def get_content_object(self):
   111     def _get_name(self):
   130         """
   112         return self.userinfo["name"]
   131         Returns the object that this comment is a comment on. Returns None if
   113     def _set_name(self, val):
   132         the object no longer exists.
   114         if self.user_id:
   133         """
   115             raise AttributeError(_("This comment was posted by an authenticated "\
   134         from django.core.exceptions import ObjectDoesNotExist
   116                                    "user and thus the name is read-only."))
   135         try:
   117         self.user_name = val
   136             return self.content_type.get_object_for_this_type(pk=self.object_id)
   118     name = property(_get_name, _set_name, doc="The name of the user who posted this comment")
   137         except ObjectDoesNotExist:
       
   138             return None
       
   139 
   119 
   140     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")
   141 
   128 
   142     def _fill_karma_cache(self):
   129     def _get_url(self):
   143         """Helper function that populates good/bad karma caches."""
   130         return self.userinfo["url"]
   144         good, bad = 0, 0
   131     def _set_url(self, val):
   145         for k in self.karmascore_set:
   132         self.user_url = val
   146             if k.score == -1:
   133     url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment")
   147                 bad +=1
       
   148             elif k.score == 1:
       
   149                 good +=1
       
   150         self._karma_total_good, self._karma_total_bad = good, bad
       
   151 
   134 
   152     def get_good_karma_total(self):
   135     def get_absolute_url(self, anchor_pattern="#c%(id)s"):
   153         if not hasattr(self, "_karma_total_good"):
   136         return self.get_content_object_url() + (anchor_pattern % self.__dict__)
   154             self._fill_karma_cache()
       
   155         return self._karma_total_good
       
   156 
       
   157     def get_bad_karma_total(self):
       
   158         if not hasattr(self, "_karma_total_bad"):
       
   159             self._fill_karma_cache()
       
   160         return self._karma_total_bad
       
   161 
       
   162     def get_karma_total(self):
       
   163         if not hasattr(self, "_karma_total_good") or not hasattr(self, "_karma_total_bad"):
       
   164             self._fill_karma_cache()
       
   165         return self._karma_total_good + self._karma_total_bad
       
   166 
   137 
   167     def get_as_text(self):
   138     def get_as_text(self):
   168         return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % \
   139         """
   169             {'user': self.user.username, 'date': self.submit_date,
   140         Return this comment as plain text.  Useful for emails.
   170             'comment': self.comment, 'domain': self.site.domain, 'url': self.get_absolute_url()}
   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
   171 
   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:
   172 
   155 
   173 class FreeComment(models.Model):
   156         * A "removal suggestion" -- where a user suggests a comment for (potential) removal.
   174     """A comment by a non-registered user."""
   157 
   175     content_type = models.ForeignKey(ContentType)
   158         * A "moderator deletion" -- used when a moderator deletes a comment.
   176     object_id = models.IntegerField(_('object ID'))
   159 
   177     comment = models.TextField(_('comment'), max_length=3000)
   160     You can (ab)use this model to add other flags, if needed. However, by
   178     person_name = models.CharField(_("person's name"), max_length=50)
   161     design users are only allowed to flag a comment with a given flag once;
   179     submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True)
   162     if you want rating look elsewhere.
   180     is_public = models.BooleanField(_('is public'))
   163     """
   181     ip_address = models.IPAddressField(_('ip address'))
   164     user      = models.ForeignKey(User, related_name="comment_flags")
   182     # TODO: Change this to is_removed, like Comment
   165     comment   = models.ForeignKey(Comment, related_name="flags")
   183     approved = models.BooleanField(_('approved by staff'))
   166     flag      = models.CharField(max_length=30, db_index=True)
   184     site = models.ForeignKey(Site)
   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"
   185 
   173 
   186     class Meta:
   174     class Meta:
   187         verbose_name = _('free comment')
   175         db_table = 'django_comment_flags'
   188         verbose_name_plural = _('free comments')
   176         unique_together = [('user', 'comment', 'flag')]
   189         ordering = ('-submit_date',)
       
   190 
       
   191     class Admin:
       
   192         fields = (
       
   193             (None, {'fields': ('content_type', 'object_id', 'site')}),
       
   194             ('Content', {'fields': ('person_name', 'comment')}),
       
   195             ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}),
       
   196         )
       
   197         list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object')
       
   198         list_filter = ('submit_date',)
       
   199         date_hierarchy = 'submit_date'
       
   200         search_fields = ('comment', 'person_name')
       
   201 
   177 
   202     def __unicode__(self):
   178     def __unicode__(self):
   203         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)
   204 
   181 
   205     def get_absolute_url(self):
   182     def save(self, force_insert=False, force_update=False):
   206         try:
   183         if self.flag_date is None:
   207             return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
   184             self.flag_date = datetime.datetime.now()
   208         except AttributeError:
   185         super(CommentFlag, self).save(force_insert, force_update)
   209             return ""
       
   210 
       
   211     def get_content_object(self):
       
   212         """
       
   213         Returns the object that this comment is a comment on. Returns None if
       
   214         the object no longer exists.
       
   215         """
       
   216         from django.core.exceptions import ObjectDoesNotExist
       
   217         try:
       
   218             return self.content_type.get_object_for_this_type(pk=self.object_id)
       
   219         except ObjectDoesNotExist:
       
   220             return None
       
   221 
       
   222     get_content_object.short_description = _('Content object')
       
   223 
       
   224 
       
   225 class KarmaScoreManager(models.Manager):
       
   226     def vote(self, user_id, comment_id, score):
       
   227         try:
       
   228             karma = self.get(comment__pk=comment_id, user__pk=user_id)
       
   229         except self.model.DoesNotExist:
       
   230             karma = self.model(None, user_id=user_id, comment_id=comment_id, score=score, scored_date=datetime.datetime.now())
       
   231             karma.save()
       
   232         else:
       
   233             karma.score = score
       
   234             karma.scored_date = datetime.datetime.now()
       
   235             karma.save()
       
   236 
       
   237     def get_pretty_score(self, score):
       
   238         """
       
   239         Given a score between -1 and 1 (inclusive), returns the same score on a
       
   240         scale between 1 and 10 (inclusive), as an integer.
       
   241         """
       
   242         if score is None:
       
   243             return DEFAULT_KARMA
       
   244         return int(round((4.5 * score) + 5.5))
       
   245 
       
   246 
       
   247 class KarmaScore(models.Model):
       
   248     user = models.ForeignKey(User)
       
   249     comment = models.ForeignKey(Comment)
       
   250     score = models.SmallIntegerField(_('score'), db_index=True)
       
   251     scored_date = models.DateTimeField(_('score date'), auto_now=True)
       
   252     objects = KarmaScoreManager()
       
   253 
       
   254     class Meta:
       
   255         verbose_name = _('karma score')
       
   256         verbose_name_plural = _('karma scores')
       
   257         unique_together = (('user', 'comment'),)
       
   258 
       
   259     def __unicode__(self):
       
   260         return _("%(score)d rating by %(user)s") % {'score': self.score, 'user': self.user}
       
   261 
       
   262 
       
   263 class UserFlagManager(models.Manager):
       
   264     def flag(self, comment, user):
       
   265         """
       
   266         Flags the given comment by the given user. If the comment has already
       
   267         been flagged by the user, or it was a comment posted by the user,
       
   268         nothing happens.
       
   269         """
       
   270         if int(comment.user_id) == int(user.id):
       
   271             return # A user can't flag his own comment. Fail silently.
       
   272         try:
       
   273             f = self.get(user__pk=user.id, comment__pk=comment.id)
       
   274         except self.model.DoesNotExist:
       
   275             from django.core.mail import mail_managers
       
   276             f = self.model(None, user.id, comment.id, None)
       
   277             message = _('This comment was flagged by %(user)s:\n\n%(text)s') % {'user': user.username, 'text': comment.get_as_text()}
       
   278             mail_managers('Comment flagged', message, fail_silently=True)
       
   279             f.save()
       
   280 
       
   281 
       
   282 class UserFlag(models.Model):
       
   283     user = models.ForeignKey(User)
       
   284     comment = models.ForeignKey(Comment)
       
   285     flag_date = models.DateTimeField(_('flag date'), auto_now_add=True)
       
   286     objects = UserFlagManager()
       
   287 
       
   288     class Meta:
       
   289         verbose_name = _('user flag')
       
   290         verbose_name_plural = _('user flags')
       
   291         unique_together = (('user', 'comment'),)
       
   292 
       
   293     def __unicode__(self):
       
   294         return _("Flag by %r") % self.user
       
   295 
       
   296 
       
   297 class ModeratorDeletion(models.Model):
       
   298     user = models.ForeignKey(User, verbose_name='moderator')
       
   299     comment = models.ForeignKey(Comment)
       
   300     deletion_date = models.DateTimeField(_('deletion date'), auto_now_add=True)
       
   301 
       
   302     class Meta:
       
   303         verbose_name = _('moderator deletion')
       
   304         verbose_name_plural = _('moderator deletions')
       
   305         unique_together = (('user', 'comment'),)
       
   306 
       
   307     def __unicode__(self):
       
   308         return _("Moderator deletion by %r") % self.user