app/django/contrib/comments/models.py
author Todd Larsen <tlarsen@google.com>
Fri, 18 Jul 2008 18:22:23 +0000
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
permissions -rw-r--r--
Major reorganization of the soc svn repo, to merge into a single App Engine image (to make development easier, now that only a single app will run all Google Open Source programs).

import datetime

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.conf import settings

MIN_PHOTO_DIMENSION = 5
MAX_PHOTO_DIMENSION = 1000

# Option codes for comment-form hidden fields.
PHOTOS_REQUIRED = 'pr'
PHOTOS_OPTIONAL = 'pa'
RATINGS_REQUIRED = 'rr'
RATINGS_OPTIONAL = 'ra'
IS_PUBLIC = 'ip'

# What users get if they don't have any karma.
DEFAULT_KARMA = 5
KARMA_NEEDED_BEFORE_DISPLAYED = 3


class CommentManager(models.Manager):
    def get_security_hash(self, options, photo_options, rating_options, target):
        """
        Returns the MD5 hash of the given options (a comma-separated string such as
        'pa,ra') and target (something like 'lcom.eventtimes:5157'). Used to
        validate that submitted form options have not been tampered-with.
        """
        import md5
        return md5.new(options + photo_options + rating_options + target + settings.SECRET_KEY).hexdigest()

    def get_rating_options(self, rating_string):
        """
        Given a rating_string, this returns a tuple of (rating_range, options).
        >>> s = "scale:1-10|First_category|Second_category"
        >>> Comment.objects.get_rating_options(s)
        ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category'])
        """
        rating_range, options = rating_string.split('|', 1)
        rating_range = range(int(rating_range[6:].split('-')[0]), int(rating_range[6:].split('-')[1])+1)
        choices = [c.replace('_', ' ') for c in options.split('|')]
        return rating_range, choices

    def get_list_with_karma(self, **kwargs):
        """
        Returns a list of Comment objects matching the given lookup terms, with
        _karma_total_good and _karma_total_bad filled.
        """
        extra_kwargs = {}
        extra_kwargs.setdefault('select', {})
        extra_kwargs['select']['_karma_total_good'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=1'
        extra_kwargs['select']['_karma_total_bad'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=-1'
        return self.filter(**kwargs).extra(**extra_kwargs)

    def user_is_moderator(self, user):
        if user.is_superuser:
            return True
        for g in user.groups.all():
            if g.id == settings.COMMENTS_MODERATORS_GROUP:
                return True
        return False


class Comment(models.Model):
    """A comment by a registered user."""
    user = models.ForeignKey(User, raw_id_admin=True)
    content_type = models.ForeignKey(ContentType)
    object_id = models.IntegerField(_('object ID'))
    headline = models.CharField(_('headline'), max_length=255, blank=True)
    comment = models.TextField(_('comment'), max_length=3000)
    rating1 = models.PositiveSmallIntegerField(_('rating #1'), blank=True, null=True)
    rating2 = models.PositiveSmallIntegerField(_('rating #2'), blank=True, null=True)
    rating3 = models.PositiveSmallIntegerField(_('rating #3'), blank=True, null=True)
    rating4 = models.PositiveSmallIntegerField(_('rating #4'), blank=True, null=True)
    rating5 = models.PositiveSmallIntegerField(_('rating #5'), blank=True, null=True)
    rating6 = models.PositiveSmallIntegerField(_('rating #6'), blank=True, null=True)
    rating7 = models.PositiveSmallIntegerField(_('rating #7'), blank=True, null=True)
    rating8 = models.PositiveSmallIntegerField(_('rating #8'), blank=True, null=True)
    # This field designates whether to use this row's ratings in aggregate
    # functions (summaries). We need this because people are allowed to post
    # multiple reviews on the same thing, but the system will only use the
    # latest one (with valid_rating=True) in tallying the reviews.
    valid_rating = models.BooleanField(_('is valid rating'))
    submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True)
    is_public = models.BooleanField(_('is public'))
    ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
    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.'))
    site = models.ForeignKey(Site)
    objects = CommentManager()

    class Meta:
        verbose_name = _('comment')
        verbose_name_plural = _('comments')
        ordering = ('-submit_date',)

    class Admin:
        fields = (
            (None, {'fields': ('content_type', 'object_id', 'site')}),
            ('Content', {'fields': ('user', 'headline', 'comment')}),
            ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}),
            ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}),
        )
        list_display = ('user', 'submit_date', 'content_type', 'get_content_object')
        list_filter = ('submit_date',)
        date_hierarchy = 'submit_date'
        search_fields = ('comment', 'user__username')

    def __unicode__(self):
        return "%s: %s..." % (self.user.username, self.comment[:100])

    def get_absolute_url(self):
        try:
            return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
        except AttributeError:
            return ""

    def get_crossdomain_url(self):
        return "/r/%d/%d/" % (self.content_type_id, self.object_id)

    def get_flag_url(self):
        return "/comments/flag/%s/" % self.id

    def get_deletion_url(self):
        return "/comments/delete/%s/" % self.id

    def get_content_object(self):
        """
        Returns the object that this comment is a comment on. Returns None if
        the object no longer exists.
        """
        from django.core.exceptions import ObjectDoesNotExist
        try:
            return self.content_type.get_object_for_this_type(pk=self.object_id)
        except ObjectDoesNotExist:
            return None

    get_content_object.short_description = _('Content object')

    def _fill_karma_cache(self):
        """Helper function that populates good/bad karma caches."""
        good, bad = 0, 0
        for k in self.karmascore_set:
            if k.score == -1:
                bad +=1
            elif k.score == 1:
                good +=1
        self._karma_total_good, self._karma_total_bad = good, bad

    def get_good_karma_total(self):
        if not hasattr(self, "_karma_total_good"):
            self._fill_karma_cache()
        return self._karma_total_good

    def get_bad_karma_total(self):
        if not hasattr(self, "_karma_total_bad"):
            self._fill_karma_cache()
        return self._karma_total_bad

    def get_karma_total(self):
        if not hasattr(self, "_karma_total_good") or not hasattr(self, "_karma_total_bad"):
            self._fill_karma_cache()
        return self._karma_total_good + self._karma_total_bad

    def get_as_text(self):
        return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % \
            {'user': self.user.username, 'date': self.submit_date,
            'comment': self.comment, 'domain': self.site.domain, 'url': self.get_absolute_url()}


class FreeComment(models.Model):
    """A comment by a non-registered user."""
    content_type = models.ForeignKey(ContentType)
    object_id = models.IntegerField(_('object ID'))
    comment = models.TextField(_('comment'), max_length=3000)
    person_name = models.CharField(_("person's name"), max_length=50)
    submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True)
    is_public = models.BooleanField(_('is public'))
    ip_address = models.IPAddressField(_('ip address'))
    # TODO: Change this to is_removed, like Comment
    approved = models.BooleanField(_('approved by staff'))
    site = models.ForeignKey(Site)

    class Meta:
        verbose_name = _('free comment')
        verbose_name_plural = _('free comments')
        ordering = ('-submit_date',)

    class Admin:
        fields = (
            (None, {'fields': ('content_type', 'object_id', 'site')}),
            ('Content', {'fields': ('person_name', 'comment')}),
            ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}),
        )
        list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object')
        list_filter = ('submit_date',)
        date_hierarchy = 'submit_date'
        search_fields = ('comment', 'person_name')

    def __unicode__(self):
        return "%s: %s..." % (self.person_name, self.comment[:100])

    def get_absolute_url(self):
        try:
            return self.get_content_object().get_absolute_url() + "#c" + str(self.id)
        except AttributeError:
            return ""

    def get_content_object(self):
        """
        Returns the object that this comment is a comment on. Returns None if
        the object no longer exists.
        """
        from django.core.exceptions import ObjectDoesNotExist
        try:
            return self.content_type.get_object_for_this_type(pk=self.object_id)
        except ObjectDoesNotExist:
            return None

    get_content_object.short_description = _('Content object')


class KarmaScoreManager(models.Manager):
    def vote(self, user_id, comment_id, score):
        try:
            karma = self.get(comment__pk=comment_id, user__pk=user_id)
        except self.model.DoesNotExist:
            karma = self.model(None, user_id=user_id, comment_id=comment_id, score=score, scored_date=datetime.datetime.now())
            karma.save()
        else:
            karma.score = score
            karma.scored_date = datetime.datetime.now()
            karma.save()

    def get_pretty_score(self, score):
        """
        Given a score between -1 and 1 (inclusive), returns the same score on a
        scale between 1 and 10 (inclusive), as an integer.
        """
        if score is None:
            return DEFAULT_KARMA
        return int(round((4.5 * score) + 5.5))


class KarmaScore(models.Model):
    user = models.ForeignKey(User)
    comment = models.ForeignKey(Comment)
    score = models.SmallIntegerField(_('score'), db_index=True)
    scored_date = models.DateTimeField(_('score date'), auto_now=True)
    objects = KarmaScoreManager()

    class Meta:
        verbose_name = _('karma score')
        verbose_name_plural = _('karma scores')
        unique_together = (('user', 'comment'),)

    def __unicode__(self):
        return _("%(score)d rating by %(user)s") % {'score': self.score, 'user': self.user}


class UserFlagManager(models.Manager):
    def flag(self, comment, user):
        """
        Flags the given comment by the given user. If the comment has already
        been flagged by the user, or it was a comment posted by the user,
        nothing happens.
        """
        if int(comment.user_id) == int(user.id):
            return # A user can't flag his own comment. Fail silently.
        try:
            f = self.get(user__pk=user.id, comment__pk=comment.id)
        except self.model.DoesNotExist:
            from django.core.mail import mail_managers
            f = self.model(None, user.id, comment.id, None)
            message = _('This comment was flagged by %(user)s:\n\n%(text)s') % {'user': user.username, 'text': comment.get_as_text()}
            mail_managers('Comment flagged', message, fail_silently=True)
            f.save()


class UserFlag(models.Model):
    user = models.ForeignKey(User)
    comment = models.ForeignKey(Comment)
    flag_date = models.DateTimeField(_('flag date'), auto_now_add=True)
    objects = UserFlagManager()

    class Meta:
        verbose_name = _('user flag')
        verbose_name_plural = _('user flags')
        unique_together = (('user', 'comment'),)

    def __unicode__(self):
        return _("Flag by %r") % self.user


class ModeratorDeletion(models.Model):
    user = models.ForeignKey(User, verbose_name='moderator')
    comment = models.ForeignKey(Comment)
    deletion_date = models.DateTimeField(_('deletion date'), auto_now_add=True)

    class Meta:
        verbose_name = _('moderator deletion')
        verbose_name_plural = _('moderator deletions')
        unique_together = (('user', 'comment'),)

    def __unicode__(self):
        return _("Moderator deletion by %r") % self.user