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
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
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
22
24
from notification import models as notification
29
31
('bbcode', 'bbcode'),
34
class PybbExcludeInternal(models.Manager):
35
def get_queryset(self):
36
return super(PybbExcludeInternal, self).get_queryset().exclude(internal=True)
33
39
class Category(models.Model):
40
"""The base model of pybb.
42
If 'internal' is set to True, the category is only visible for superusers and
43
users which have the permission 'can_access_internal'.
34
46
name = models.CharField(_('Name'), max_length=80)
35
47
position = models.IntegerField(_('Position'), blank=True, default=0)
48
internal = models.BooleanField(
50
verbose_name=_('Internal Category'),
51
help_text=_('If set, this category is only visible for special users.')
54
objects = models.Manager()
55
exclude_internal = PybbExcludeInternal()
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"),)
42
def __unicode__(self):
45
67
def forum_count(self):
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(
91
on_delete=models.CASCADE,
95
help_text='Users in this Group will have administrative permissions in this Forum.',
69
99
ordering = ['position']
70
100
verbose_name = _('Forum')
71
101
verbose_name_plural = _('Forums')
73
def __unicode__(self):
76
106
def topic_count(self):
84
return Post.objects.filter(topic__forum=self).select_related()
114
return Post.objects.filter(topic__forum=self).exclude(hidden=True).select_related()
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()
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]
129
posts = topic.posts.exclude(hidden=True).order_by(
130
'-created').select_related()
95
135
except IndexError:
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)
111
if settings.USE_SPHINX:
112
search = SphinxSearch(
149
subscribers = models.ManyToManyField(
150
User, related_name='subscriptions', verbose_name=_('Subscribers'), blank=True)
119
153
ordering = ['-updated']
120
154
verbose_name = _('Topic')
121
155
verbose_name_plural = _('Topics')
123
def __unicode__(self):
128
return self.posts.all().order_by('created').select_related()[0]
163
return self.posts.all().order_by('created').select_related()[0]
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]
173
# If the first post of this topic is hidden, the topic is hidden
175
return self.posts.first().hidden
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()
138
183
def get_absolute_url(self):
139
184
return reverse('pybb_topic', args=[self.id])
150
195
read.time = datetime.now()
153
#def has_unreads(self, user):
198
# def has_unreads(self, user):
155
200
#read = Read.objects.get(user=user, topic=self)
156
#except Read.DoesNotExist:
159
#return self.updated > read.time
201
# except Read.DoesNotExist:
204
# return self.updated > read.time
162
207
class RenderableItem(models.Model):
164
Base class for models that has markup, body, body_text and body_html fields.
208
"""Base class for models that has markup, body, body_text and body_html
184
229
self.body_html = urlize(self.body_html)
232
class HiddenTopicsManager(models.Manager):
233
"""Find all hidden topics by posts.
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
239
Post.objects.exclude(topic__in=Post.hidden_topics.all()).filter(...)
241
Use this with caution, because it affects performance, see:
242
https://docs.djangoproject.com/en/dev/ref/models/querysets/#in
245
def get_queryset(self, *args, **kwargs):
246
qs = super(HiddenTopicsManager,
247
self).get_queryset().filter(hidden=True)
252
if post.topic.is_hidden:
253
hidden_topics.append(post.topic)
258
class PublicPostsManager(models.Manager):
260
def public(self, limit=None, date_from=None):
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:
267
limit: Slice the QuerySet [:limit].
268
date_from: Gathers all posts from this day until today.
271
qs = self.get_queryset().filter(
272
topic__forum__category__internal=False, hidden=False).exclude(
273
topic__in=Post.hidden_topics.all()).order_by(
277
qs = qs.filter(created__gte=date_from)
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='')
199
if settings.USE_SPHINX:
200
search = SphinxSearch(
296
hidden = models.BooleanField(_('Hidden'), blank=True, default=False)
298
objects = PublicPostsManager() # Normal manager, extended
299
hidden_topics = HiddenTopicsManager() # Custom manager
209
302
ordering = ['created']
234
327
super(Post, self).save(*args, **kwargs)
237
329
def get_absolute_url(self):
238
330
return reverse('pybb_post', args=[self.id])
332
def unhide_post(self):
333
"""Unhide post(s) and inform subscribers."""
336
if self.topic.post_count == 1:
338
send(User.objects.all(), 'forum_new_topic',
339
{'topic': self.topic, 'post': self, 'user': self.topic.user})
341
# Inform topic subscribers
342
send(self.topic.subscribers.all(), 'forum_new_post',
343
{'post': self, 'topic': self.topic, 'user': self.user})
241
345
def delete(self, *args, **kwargs):
242
346
self_id = self.id
270
380
self.time = datetime.now()
271
381
super(Read, self).save(*args, **kwargs)
274
def __unicode__(self):
275
return u'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, unicode(self.time))
278
class PrivateMessage(RenderableItem):
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'))
291
ordering = ['-created']
292
verbose_name = _('Private message')
293
verbose_name_plural = _('Private messages')
295
# TODO: summary and part of the save method is the same as in the Post model
296
# move to common functions
299
tail = len(self.body) > LIMIT and '...' or ''
300
return self.body[:LIMIT] + tail
302
def __unicode__(self):
305
def save(self, *args, **kwargs):
306
if self.created is None:
307
self.created = datetime.now()
310
new = self.id is None
311
super(PrivateMessage, self).save(*args, **kwargs)
313
def get_absolute_url(self):
314
return reverse('pybb_show_pm', args=[self.id])
384
return 'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, str(self.time))
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)
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)
331
def __unicode__(self):
334
407
def get_absolute_url(self):