summaryrefslogtreecommitdiffstats
path: root/webapp/django/contrib/comments/models.py
blob: fdf34c8997c78070c0d90b8826da9d5e7a1dbc57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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.
        """
        from django.utils.hashcompat import md5_constructor
        return md5_constructor(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)
    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',)

    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',)

    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