1
from datetime import datetime
2
from mainpage.templatetags.wl_markdown import do_wl_markdown
6
from django.db import models
7
from django.contrib.auth.models import User
8
from django.urls import reverse
9
from django.utils.html import strip_tags
10
from django.utils.translation import ugettext_lazy as _
11
from django.conf import settings
13
from pybb.markups import mypostmarkup
14
from pybb.util import urlize, memoize_method, unescape
15
from pybb import settings as pybb_settings
17
from django.conf import settings
18
from notification.models import send
19
from django.contrib.auth.models import User
20
from check_input.models import SuspiciousInput
24
from notification import models as notification
25
from django.db.models import signals
30
('markdown', 'markdown'),
35
class Category(models.Model):
36
name = models.CharField(_('Name'), max_length=80)
37
position = models.IntegerField(_('Position'), blank=True, default=0)
40
ordering = ['position']
41
verbose_name = _('Category')
42
verbose_name_plural = _('Categories')
44
def __unicode__(self):
47
def forum_count(self):
48
return self.forums.all().count()
50
def get_absolute_url(self):
51
return reverse('pybb_category', args=[self.id])
55
return Topic.objects.filter(forum__category=self).select_related()
59
return Post.objects.filter(topic__forum__category=self).select_related()
62
class Forum(models.Model):
63
category = models.ForeignKey(
64
Category, related_name='forums', verbose_name=_('Category'))
65
name = models.CharField(_('Name'), max_length=80)
66
position = models.IntegerField(_('Position'), blank=True, default=0)
67
description = models.TextField(_('Description'), blank=True, default='')
68
moderators = models.ManyToManyField(
69
User, blank=True, verbose_name=_('Moderators'))
70
updated = models.DateTimeField(_('Updated'), null=True)
73
ordering = ['position']
74
verbose_name = _('Forum')
75
verbose_name_plural = _('Forums')
77
def __unicode__(self):
80
def topic_count(self):
81
return self.topics.all().count()
83
def get_absolute_url(self):
84
return reverse('pybb_forum', args=[self.id])
88
return Post.objects.filter(topic__forum=self).exclude(hidden=True).select_related()
92
return Post.objects.filter(topic__forum=self).exclude(hidden=True).count()
96
# This has better performance than using the posts manager hidden_topics
97
# We search only for the last 10 topics
98
topics = self.topics.order_by('-updated')[:10]
102
posts = topic.posts.exclude(hidden=True).order_by(
103
'-created').select_related()
112
class Topic(models.Model):
113
forum = models.ForeignKey(
114
Forum, related_name='topics', verbose_name=_('Forum'))
115
name = models.CharField(_('Subject'), max_length=255)
116
created = models.DateTimeField(_('Created'), null=True)
117
updated = models.DateTimeField(_('Updated'), null=True)
118
user = models.ForeignKey(User, verbose_name=_('User'))
119
views = models.IntegerField(_('Views count'), blank=True, default=0)
120
sticky = models.BooleanField(_('Sticky'), blank=True, default=False)
121
closed = models.BooleanField(_('Closed'), blank=True, default=False)
122
subscribers = models.ManyToManyField(
123
User, related_name='subscriptions', verbose_name=_('Subscribers'), blank=True)
126
ordering = ['-updated']
127
verbose_name = _('Topic')
128
verbose_name_plural = _('Topics')
130
def __unicode__(self):
136
return self.posts.all().order_by('created').select_related()[0]
142
return self.posts.exclude(hidden=True).order_by('-created').select_related()[0]
146
# If the first post of this topic is hidden, the topic is hidden
148
return self.posts.first().hidden
153
def post_count(self):
154
return Post.objects.filter(topic=self).exclude(hidden=True).count()
156
def get_absolute_url(self):
157
return reverse('pybb_topic', args=[self.id])
159
def save(self, *args, **kwargs):
160
new = self.id is None
162
self.created = datetime.now()
163
super(Topic, self).save(*args, **kwargs)
165
def update_read(self, user):
166
read, new = Read.objects.get_or_create(user=user, topic=self)
168
read.time = datetime.now()
171
# def has_unreads(self, user):
173
#read = Read.objects.get(user=user, topic=self)
174
# except Read.DoesNotExist:
177
# return self.updated > read.time
180
class RenderableItem(models.Model):
181
"""Base class for models that has markup, body, body_text and body_html
188
if self.markup == 'bbcode':
189
self.body_html = mypostmarkup.markup(self.body, auto_urls=False)
190
elif self.markup == 'markdown':
191
self.body_html = unicode(do_wl_markdown(
192
self.body, 'bleachit'))
194
raise Exception('Invalid markup property: %s' % self.markup)
196
# Remove tags which was generated with the markup processor
197
text = strip_tags(self.body_html)
199
# Unescape entities which was generated with the markup processor
200
self.body_text = unescape(text)
202
self.body_html = urlize(self.body_html)
205
class HiddenTopicsManager(models.Manager):
206
"""Find all hidden topics by posts.
208
A whole topic is hidden, if the first post is hidden.
209
This manager returns the hidden topics and can be used to filter them out
212
Post.objects.exclude(topic__in=Post.hidden_topics.all()).filter(...)
214
Use this with caution, because it affects performance, see:
215
https://docs.djangoproject.com/en/dev/ref/models/querysets/#in
218
def get_queryset(self, *args, **kwargs):
219
qs = super(HiddenTopicsManager,
220
self).get_queryset().filter(hidden=True)
225
if post.topic.is_hidden:
226
hidden_topics.append(post.topic)
232
class Post(RenderableItem):
233
topic = models.ForeignKey(
234
Topic, related_name='posts', verbose_name=_('Topic'))
235
user = models.ForeignKey(
236
User, related_name='posts', verbose_name=_('User'))
237
created = models.DateTimeField(_('Created'), blank=True)
238
updated = models.DateTimeField(_('Updated'), blank=True, null=True)
239
markup = models.CharField(_('Markup'), max_length=15,
240
default=pybb_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
241
body = models.TextField(_('Message'))
242
body_html = models.TextField(_('HTML version'))
243
body_text = models.TextField(_('Text version'))
244
hidden = models.BooleanField(_('Hidden'), blank=True, default=False)
246
objects = models.Manager() # Normal manager
247
hidden_topics = HiddenTopicsManager() # Custom manager
250
ordering = ['created']
251
verbose_name = _('Post')
252
verbose_name_plural = _('Posts')
256
tail = len(self.body) > LIMIT and '...' or ''
257
return self.body[:LIMIT] + tail
259
__unicode__ = summary
261
def save(self, *args, **kwargs):
262
if self.created is None:
263
self.created = datetime.now()
267
new = self.id is None
270
self.topic.updated = datetime.now()
272
self.topic.forum.updated = self.topic.updated
273
self.topic.forum.save()
275
super(Post, self).save(*args, **kwargs)
277
def get_absolute_url(self):
278
return reverse('pybb_post', args=[self.id])
280
def unhide_post(self):
281
"""Unhide post(s) and inform subscribers."""
284
if self.topic.post_count == 1:
286
send(User.objects.all(), 'forum_new_topic',
287
{'topic': self.topic, 'post': self, 'user': self.topic.user})
289
# Inform topic subscribers
290
send(self.topic.subscribers.all(), 'forum_new_post',
291
{'post': self, 'topic': self.topic, 'user': self.user})
293
def delete(self, *args, **kwargs):
295
head_post_id = self.topic.posts.order_by('created')[0].id
296
super(Post, self).delete(*args, **kwargs)
299
self.topic.forum.save()
301
if self_id == head_post_id:
306
SuspiciousInput.objects.get(object_id = self.pk)
313
class Read(models.Model):
314
"""For each topic that user has entered the time is logged to this
317
user = models.ForeignKey(User, verbose_name=_('User'))
318
topic = models.ForeignKey(Topic, verbose_name=_('Topic'))
319
time = models.DateTimeField(_('Time'), blank=True)
322
unique_together = ['user', 'topic']
323
verbose_name = _('Read')
324
verbose_name_plural = _('Reads')
326
def save(self, *args, **kwargs):
327
if self.time is None:
328
self.time = datetime.now()
329
super(Read, self).save(*args, **kwargs)
331
def __unicode__(self):
332
return u'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, unicode(self.time))
335
class PrivateMessage(RenderableItem):
337
dst_user = models.ForeignKey(User, verbose_name=_(
338
'Recipient'), related_name='dst_users')
339
src_user = models.ForeignKey(User, verbose_name=_(
340
'Author'), related_name='src_users')
341
read = models.BooleanField(_('Read'), blank=True, default=False)
342
created = models.DateTimeField(_('Created'), blank=True)
343
markup = models.CharField(_('Markup'), max_length=15,
344
default=pybb_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
345
subject = models.CharField(_('Subject'), max_length=255)
346
body = models.TextField(_('Message'))
347
body_html = models.TextField(_('HTML version'))
348
body_text = models.TextField(_('Text version'))
351
ordering = ['-created']
352
verbose_name = _('Private message')
353
verbose_name_plural = _('Private messages')
355
# TODO: summary and part of the save method is the same as in the Post model
356
# move to common functions
359
tail = len(self.body) > LIMIT and '...' or ''
360
return self.body[:LIMIT] + tail
362
def __unicode__(self):
365
def save(self, *args, **kwargs):
366
if self.created is None:
367
self.created = datetime.now()
370
new = self.id is None
371
super(PrivateMessage, self).save(*args, **kwargs)
373
def get_absolute_url(self):
374
return reverse('pybb_show_pm', args=[self.id])
377
class Attachment(models.Model):
378
post = models.ForeignKey(Post, verbose_name=_(
379
'Post'), related_name='attachments')
380
size = models.IntegerField(_('Size'))
381
content_type = models.CharField(_('Content type'), max_length=255)
382
path = models.CharField(_('Path'), max_length=255)
383
name = models.TextField(_('Name'))
384
hash = models.CharField(_('Hash'), max_length=40,
385
blank=True, default='', db_index=True)
387
def save(self, *args, **kwargs):
388
super(Attachment, self).save(*args, **kwargs)
390
self.hash = hashlib.sha1(
391
str(self.id) + settings.SECRET_KEY).hexdigest()
392
super(Attachment, self).save(*args, **kwargs)
394
def __unicode__(self):
397
def get_absolute_url(self):
398
return reverse('pybb_attachment', args=[self.hash])
400
def size_display(self):
404
elif size < 1024 * 1024:
405
return '%dKb' % int(size / 1024)
407
return '%.2fMb' % (size / float(1024 * 1024))
409
def get_absolute_path(self):
410
return os.path.join(settings.MEDIA_ROOT, pybb_settings.ATTACHMENT_UPLOAD_TO,
414
if notification is not None:
415
signals.post_save.connect(notification.handle_observations, sender=Post)
417
from pybb import signals
418
signals.setup_signals()