~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to pybb/models.py

  • Committer: kaputtnik
  • Date: 2019-05-30 18:20:02 UTC
  • mto: This revision was merged to the branch mainline in revision 540.
  • Revision ID: kaputtnik-20190530182002-g7l91m1xo28clghv
adjusted README; first commit on the new server

Show diffs side-by-side

added added

removed removed

Lines of Context:
5
5
 
6
6
from django.db import models
7
7
from django.contrib.auth.models import User
8
 
from django.core.urlresolvers import reverse
 
8
from django.contrib.auth.models import Group
 
9
from django.urls import reverse
9
10
from django.utils.html import strip_tags
10
11
from django.utils.translation import ugettext_lazy as _
11
12
from django.conf import settings
12
13
 
13
14
from pybb.markups import mypostmarkup
14
 
from pybb.util import urlize, memoize_method, unescape
 
15
from pybb.util import urlize, unescape
15
16
from pybb import settings as pybb_settings
16
17
 
17
18
from django.conf import settings
18
 
if settings.USE_SPHINX:
19
 
    from djangosphinx.models import SphinxSearch
 
19
from notification.models import send
 
20
from check_input.models import SuspiciousInput
 
21
 
20
22
 
21
23
try:
22
24
    from notification import models as notification
29
31
    ('bbcode', 'bbcode'),
30
32
)
31
33
 
 
34
class PybbExcludeInternal(models.Manager):
 
35
    def get_queryset(self):
 
36
        return super(PybbExcludeInternal, self).get_queryset().exclude(internal=True)
 
37
 
32
38
 
33
39
class Category(models.Model):
 
40
    """The base model of pybb.
 
41
    
 
42
    If 'internal' is set to True, the category is only visible for superusers and
 
43
    users which have the permission 'can_access_internal'.
 
44
    """
 
45
    
34
46
    name = models.CharField(_('Name'), max_length=80)
35
47
    position = models.IntegerField(_('Position'), blank=True, default=0)
 
48
    internal = models.BooleanField(
 
49
        default=False,
 
50
        verbose_name=_('Internal Category'),
 
51
        help_text=_('If set, this category is only visible for special users.')
 
52
        )
 
53
 
 
54
    objects = models.Manager()
 
55
    exclude_internal = PybbExcludeInternal()
36
56
 
37
57
    class Meta:
38
58
        ordering = ['position']
39
59
        verbose_name = _('Category')
40
60
        verbose_name_plural = _('Categories')
 
61
        # See also settings.INTERNAL_PERM
 
62
        permissions = (("can_access_internal", "Can access Internal Forums"),)
41
63
 
42
 
    def __unicode__(self):
 
64
    def __str__(self):
43
65
        return self.name
44
66
 
45
67
    def forum_count(self):
58
80
 
59
81
 
60
82
class Forum(models.Model):
61
 
    category = models.ForeignKey(Category, related_name='forums', verbose_name=_('Category'))
 
83
    category = models.ForeignKey(
 
84
        Category, related_name='forums', verbose_name=_('Category'))
62
85
    name = models.CharField(_('Name'), max_length=80)
63
86
    position = models.IntegerField(_('Position'), blank=True, default=0)
64
87
    description = models.TextField(_('Description'), blank=True, default='')
65
 
    moderators = models.ManyToManyField(User, blank=True, null=True, verbose_name=_('Moderators'))
66
88
    updated = models.DateTimeField(_('Updated'), null=True)
 
89
    moderator_group = models.ForeignKey(
 
90
        Group,
 
91
        on_delete=models.CASCADE,
 
92
        blank=True,
 
93
        null=True,
 
94
        default=None,
 
95
        help_text='Users in this Group will have administrative permissions in this Forum.',
 
96
    )
67
97
 
68
98
    class Meta:
69
99
        ordering = ['position']
70
100
        verbose_name = _('Forum')
71
101
        verbose_name_plural = _('Forums')
72
102
 
73
 
    def __unicode__(self):
 
103
    def __str__(self):
74
104
        return self.name
75
105
 
76
106
    def topic_count(self):
81
111
 
82
112
    @property
83
113
    def posts(self):
84
 
        return Post.objects.filter(topic__forum=self).select_related()
 
114
        return Post.objects.filter(topic__forum=self).exclude(hidden=True).select_related()
85
115
 
86
116
    @property
87
117
    def post_count(self):
88
 
        return Post.objects.filter(topic__forum=self).count()
 
118
        return Post.objects.filter(topic__forum=self).exclude(hidden=True).count()
89
119
 
90
120
    @property
91
121
    def last_post(self):
92
 
        posts = self.posts.order_by('-created').select_related()
 
122
        # This has better performance than using the posts manager hidden_topics
 
123
        # We search only for the last 10 topics
 
124
        topics = self.topics.order_by('-updated')[:10]
 
125
        posts = []
 
126
        for topic in topics:
 
127
            if topic.is_hidden:
 
128
                continue
 
129
            posts = topic.posts.exclude(hidden=True).order_by(
 
130
            '-created').select_related()
 
131
            break
 
132
 
93
133
        try:
94
134
            return posts[0]
95
135
        except IndexError:
97
137
 
98
138
 
99
139
class Topic(models.Model):
100
 
    forum = models.ForeignKey(Forum, related_name='topics', verbose_name=_('Forum'))
 
140
    forum = models.ForeignKey(
 
141
        Forum, related_name='topics', verbose_name=_('Forum'))
101
142
    name = models.CharField(_('Subject'), max_length=255)
102
143
    created = models.DateTimeField(_('Created'), null=True)
103
144
    updated = models.DateTimeField(_('Updated'), null=True)
105
146
    views = models.IntegerField(_('Views count'), blank=True, default=0)
106
147
    sticky = models.BooleanField(_('Sticky'), blank=True, default=False)
107
148
    closed = models.BooleanField(_('Closed'), blank=True, default=False)
108
 
    subscribers = models.ManyToManyField(User, related_name='subscriptions', verbose_name=_('Subscribers'), blank=True)
109
 
 
110
 
    # Django sphinx
111
 
    if settings.USE_SPHINX:
112
 
        search = SphinxSearch(
113
 
            weights = {
114
 
                'name': 100,
115
 
                }
116
 
            )
 
149
    subscribers = models.ManyToManyField(
 
150
        User, related_name='subscriptions', verbose_name=_('Subscribers'), blank=True)
117
151
 
118
152
    class Meta:
119
153
        ordering = ['-updated']
120
154
        verbose_name = _('Topic')
121
155
        verbose_name_plural = _('Topics')
122
156
 
123
 
    def __unicode__(self):
 
157
    def __str__(self):
124
158
        return self.name
125
159
 
126
160
    @property
127
161
    def head(self):
128
 
        return self.posts.all().order_by('created').select_related()[0]
 
162
        try:
 
163
            return self.posts.all().order_by('created').select_related()[0]
 
164
        except:
 
165
            return None
129
166
 
130
167
    @property
131
168
    def last_post(self):
132
 
        return self.posts.all().order_by('-created').select_related()[0]
 
169
        return self.posts.exclude(hidden=True).order_by('-created').select_related()[0]
 
170
 
 
171
    @property
 
172
    def is_hidden(self):
 
173
        # If the first post of this topic is hidden, the topic is hidden
 
174
        try:
 
175
            return self.posts.first().hidden
 
176
        except:
 
177
            return False
133
178
 
134
179
    @property
135
180
    def post_count(self):
136
 
        return Post.objects.filter(topic=self).count()
 
181
        return Post.objects.filter(topic=self).exclude(hidden=True).count()
137
182
 
138
183
    def get_absolute_url(self):
139
184
        return reverse('pybb_topic', args=[self.id])
150
195
            read.time = datetime.now()
151
196
            read.save()
152
197
 
153
 
    #def has_unreads(self, user):
154
 
        #try:
 
198
    # def has_unreads(self, user):
 
199
        # try:
155
200
            #read = Read.objects.get(user=user, topic=self)
156
 
        #except Read.DoesNotExist:
157
 
            #return True
158
 
        #else:
159
 
            #return self.updated > read.time
 
201
        # except Read.DoesNotExist:
 
202
            # return True
 
203
        # else:
 
204
            # return self.updated > read.time
160
205
 
161
206
 
162
207
class RenderableItem(models.Model):
163
 
    """
164
 
    Base class for models that has markup, body, body_text and body_html fields.
165
 
    """
 
208
    """Base class for models that has markup, body, body_text and body_html
 
209
    fields."""
166
210
 
167
211
    class Meta:
168
212
        abstract = True
171
215
        if self.markup == 'bbcode':
172
216
            self.body_html = mypostmarkup.markup(self.body, auto_urls=False)
173
217
        elif self.markup == 'markdown':
174
 
            self.body_html = unicode(do_wl_markdown(self.body, safe_mode='escape', wikiwords=False))
 
218
            self.body_html = str(do_wl_markdown(
 
219
                self.body, 'bleachit'))
175
220
        else:
176
221
            raise Exception('Invalid markup property: %s' % self.markup)
177
222
 
184
229
        self.body_html = urlize(self.body_html)
185
230
 
186
231
 
 
232
class HiddenTopicsManager(models.Manager):
 
233
    """Find all hidden topics by posts.
 
234
 
 
235
    A whole topic is hidden, if the first post is hidden.
 
236
    This manager returns the hidden topics and can be used to filter them out
 
237
    like so:
 
238
 
 
239
    Post.objects.exclude(topic__in=Post.hidden_topics.all()).filter(...)
 
240
 
 
241
    Use this with caution, because it affects performance, see:
 
242
    https://docs.djangoproject.com/en/dev/ref/models/querysets/#in
 
243
    """
 
244
 
 
245
    def get_queryset(self, *args, **kwargs):
 
246
        qs = super(HiddenTopicsManager,
 
247
                   self).get_queryset().filter(hidden=True)
 
248
 
 
249
        hidden_topics = []
 
250
        try:
 
251
            for post in qs:
 
252
                if post.topic.is_hidden:
 
253
                    hidden_topics.append(post.topic)
 
254
            return hidden_topics
 
255
        except:
 
256
            return []
 
257
 
 
258
class PublicPostsManager(models.Manager):
 
259
 
 
260
    def public(self, limit=None, date_from=None):
 
261
        """Get public posts.
 
262
 
 
263
        Filters out all posts which shouldn't be visible to
 
264
        normal visitors. The result is always orderd by the
 
265
        posts creation time, Descending. Optional arguments:
 
266
 
 
267
        limit:     Slice the QuerySet [:limit].
 
268
        date_from: Gathers all posts from this day until today.
 
269
        """
 
270
 
 
271
        qs = self.get_queryset().filter(
 
272
            topic__forum__category__internal=False, hidden=False).exclude(
 
273
            topic__in=Post.hidden_topics.all()).order_by(
 
274
            '-created')
 
275
        
 
276
        if date_from:
 
277
            qs = qs.filter(created__gte=date_from)
 
278
        if limit:
 
279
            qs = qs[:limit]
 
280
 
 
281
        return qs
 
282
 
 
283
 
187
284
class Post(RenderableItem):
188
 
    topic = models.ForeignKey(Topic, related_name='posts', verbose_name=_('Topic'))
189
 
    user = models.ForeignKey(User, related_name='posts', verbose_name=_('User'))
 
285
    topic = models.ForeignKey(
 
286
        Topic, related_name='posts', verbose_name=_('Topic'))
 
287
    user = models.ForeignKey(
 
288
        User, related_name='posts', verbose_name=_('User'))
190
289
    created = models.DateTimeField(_('Created'), blank=True)
191
290
    updated = models.DateTimeField(_('Updated'), blank=True, null=True)
192
 
    markup = models.CharField(_('Markup'), max_length=15, default=pybb_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
 
291
    markup = models.CharField(_('Markup'), max_length=15,
 
292
                              default=pybb_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
193
293
    body = models.TextField(_('Message'))
194
294
    body_html = models.TextField(_('HTML version'))
195
295
    body_text = models.TextField(_('Text version'))
196
 
    user_ip = models.IPAddressField(_('User IP'), blank=True, default='')
197
 
 
198
 
    # Django sphinx
199
 
    if settings.USE_SPHINX:
200
 
        search = SphinxSearch(
201
 
            weights = {
202
 
                'body_text': 100,
203
 
                'body_html': 0,
204
 
                }
205
 
            )
206
 
 
 
296
    hidden = models.BooleanField(_('Hidden'), blank=True, default=False)
 
297
 
 
298
    objects = PublicPostsManager()  # Normal manager, extended
 
299
    hidden_topics = HiddenTopicsManager()  # Custom manager
207
300
 
208
301
    class Meta:
209
302
        ordering = ['created']
215
308
        tail = len(self.body) > LIMIT and '...' or ''
216
309
        return self.body[:LIMIT] + tail
217
310
 
218
 
    __unicode__ = summary
 
311
    __str__ = summary
219
312
 
220
313
    def save(self, *args, **kwargs):
221
314
        if self.created is None:
233
326
 
234
327
        super(Post, self).save(*args, **kwargs)
235
328
 
236
 
 
237
329
    def get_absolute_url(self):
238
330
        return reverse('pybb_post', args=[self.id])
239
331
 
 
332
    def unhide_post(self):
 
333
        """Unhide post(s) and inform subscribers."""
 
334
        self.hidden = False
 
335
        self.save()
 
336
        if self.topic.post_count == 1:
 
337
            # The topic is new
 
338
            send(User.objects.all(), 'forum_new_topic',
 
339
                 {'topic': self.topic, 'post': self, 'user': self.topic.user})
 
340
        else:
 
341
            # Inform topic subscribers
 
342
            send(self.topic.subscribers.all(), 'forum_new_post',
 
343
                 {'post': self, 'topic': self.topic, 'user': self.user})
240
344
 
241
345
    def delete(self, *args, **kwargs):
242
346
        self_id = self.id
249
353
        if self_id == head_post_id:
250
354
            self.topic.delete()
251
355
 
 
356
    def is_spam(self):
 
357
        try:
 
358
            SuspiciousInput.objects.get(object_id = self.pk)
 
359
            return True
 
360
        except:
 
361
            pass
 
362
        return False
 
363
 
252
364
 
253
365
class Read(models.Model):
254
 
    """
255
 
    For each topic that user has entered the time
256
 
    is logged to this model.
257
 
    """
 
366
    """For each topic that user has entered the time is logged to this
 
367
    model."""
258
368
 
259
369
    user = models.ForeignKey(User, verbose_name=_('User'))
260
370
    topic = models.ForeignKey(Topic, verbose_name=_('Topic'))
270
380
            self.time = datetime.now()
271
381
        super(Read, self).save(*args, **kwargs)
272
382
 
273
 
 
274
 
    def __unicode__(self):
275
 
        return u'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, unicode(self.time))
276
 
 
277
 
 
278
 
class PrivateMessage(RenderableItem):
279
 
 
280
 
    dst_user = models.ForeignKey(User, verbose_name=_('Recipient'), related_name='dst_users')
281
 
    src_user = models.ForeignKey(User, verbose_name=_('Author'), related_name='src_users')
282
 
    read = models.BooleanField(_('Read'), blank=True, default=False)
283
 
    created = models.DateTimeField(_('Created'), blank=True)
284
 
    markup = models.CharField(_('Markup'), max_length=15, default=pybb_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
285
 
    subject = models.CharField(_('Subject'), max_length=255)
286
 
    body = models.TextField(_('Message'))
287
 
    body_html = models.TextField(_('HTML version'))
288
 
    body_text = models.TextField(_('Text version'))
289
 
 
290
 
    class Meta:
291
 
        ordering = ['-created']
292
 
        verbose_name = _('Private message')
293
 
        verbose_name_plural = _('Private messages')
294
 
 
295
 
    # TODO: summary and part of the save method is the same as in the Post model
296
 
    # move to common functions
297
 
    def summary(self):
298
 
        LIMIT = 50
299
 
        tail = len(self.body) > LIMIT and '...' or ''
300
 
        return self.body[:LIMIT] + tail
301
 
 
302
 
    def __unicode__(self):
303
 
        return self.subject
304
 
 
305
 
    def save(self, *args, **kwargs):
306
 
        if self.created is None:
307
 
            self.created = datetime.now()
308
 
        self.render()
309
 
 
310
 
        new = self.id is None
311
 
        super(PrivateMessage, self).save(*args, **kwargs)
312
 
 
313
 
    def get_absolute_url(self):
314
 
        return  reverse('pybb_show_pm', args=[self.id])
 
383
    def __str__(self):
 
384
        return 'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, str(self.time))
315
385
 
316
386
 
317
387
class Attachment(models.Model):
318
 
    post = models.ForeignKey(Post, verbose_name=_('Post'), related_name='attachments')
 
388
    post = models.ForeignKey(Post, verbose_name=_(
 
389
        'Post'), related_name='attachments')
319
390
    size = models.IntegerField(_('Size'))
320
391
    content_type = models.CharField(_('Content type'), max_length=255)
321
392
    path = models.CharField(_('Path'), max_length=255)
322
393
    name = models.TextField(_('Name'))
323
 
    hash = models.CharField(_('Hash'), max_length=40, blank=True, default='', db_index=True)
 
394
    hash = models.CharField(_('Hash'), max_length=40,
 
395
                            blank=True, default='', db_index=True)
324
396
 
325
397
    def save(self, *args, **kwargs):
326
398
        super(Attachment, self).save(*args, **kwargs)
327
399
        if not self.hash:
328
 
            self.hash = hashlib.sha1(str(self.id) + settings.SECRET_KEY).hexdigest()
 
400
            self.hash = hashlib.sha1(
 
401
                str(self.id) + settings.SECRET_KEY).hexdigest()
329
402
        super(Attachment, self).save(*args, **kwargs)
330
403
 
331
 
    def __unicode__(self):
 
404
    def __str__(self):
332
405
        return self.name
333
406
 
334
407
    def get_absolute_url(self):
343
416
        else:
344
417
            return '%.2fMb' % (size / float(1024 * 1024))
345
418
 
346
 
 
347
419
    def get_absolute_path(self):
348
420
        return os.path.join(settings.MEDIA_ROOT, pybb_settings.ATTACHMENT_UPLOAD_TO,
349
421
                            self.path)
350
422
 
351
423
 
352
 
#if notification is not None:
353
 
#    signals.post_save.connect(notification.handle_observations, sender=Post)
 
424
if notification is not None:
 
425
    signals.post_save.connect(notification.handle_observations, sender=Post)
354
426
 
355
427
from pybb import signals
356
428
signals.setup_signals()