1
from datetime import datetime
2
from markdown import Markdown
6
from django.db import models
7
from django.contrib.auth.models import User
8
from django.core.urlresolvers 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.fields import AutoOneToOneField, ExtendedImageField
15
from pybb.util import urlize, memoize_method, unescape
16
from pybb import settings as pybb_settings
18
TZ_CHOICES = [(float(x[0]), x[1]) for x in (
19
(-12, '-12'), (-11, '-11'), (-10, '-10'), (-9.5, '-09.5'), (-9, '-09'),
20
(-8.5, '-08.5'), (-8, '-08 PST'), (-7, '-07 MST'), (-6, '-06 CST'),
21
(-5, '-05 EST'), (-4, '-04 AST'), (-3.5, '-03.5'), (-3, '-03 ADT'),
22
(-2, '-02'), (-1, '-01'), (0, '00 GMT'), (1, '+01 CET'), (2, '+02'),
23
(3, '+03'), (3.5, '+03.5'), (4, '+04'), (4.5, '+04.5'), (5, '+05'),
24
(5.5, '+05.5'), (6, '+06'), (6.5, '+06.5'), (7, '+07'), (8, '+08'),
25
(9, '+09'), (9.5, '+09.5'), (10, '+10'), (10.5, '+10.5'), (11, '+11'),
26
(11.5, '+11.5'), (12, '+12'), (13, '+13'), (14, '+14'),
31
('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(Category, related_name='forums', verbose_name=_('Category'))
64
name = models.CharField(_('Name'), max_length=80)
65
position = models.IntegerField(_('Position'), blank=True, default=0)
66
description = models.TextField(_('Description'), blank=True, default='')
67
moderators = models.ManyToManyField(User, blank=True, null=True, verbose_name=_('Moderators'))
68
updated = models.DateTimeField(_('Updated'), null=True)
69
post_count = models.IntegerField(_('Post count'), blank=True, default=0)
72
ordering = ['position']
73
verbose_name = _('Forum')
74
verbose_name_plural = _('Forums')
76
def __unicode__(self):
79
def topic_count(self):
80
return self.topics.all().count()
82
def get_absolute_url(self):
83
return reverse('pybb_forum', args=[self.id])
87
return Post.objects.filter(topic__forum=self).select_related()
91
posts = self.posts.order_by('-created').select_related()
98
class Topic(models.Model):
99
forum = models.ForeignKey(Forum, related_name='topics', verbose_name=_('Forum'))
100
name = models.CharField(_('Subject'), max_length=255)
101
created = models.DateTimeField(_('Created'), null=True)
102
updated = models.DateTimeField(_('Updated'), null=True)
103
user = models.ForeignKey(User, verbose_name=_('User'))
104
views = models.IntegerField(_('Views count'), blank=True, default=0)
105
sticky = models.BooleanField(_('Sticky'), blank=True, default=False)
106
closed = models.BooleanField(_('Closed'), blank=True, default=False)
107
subscribers = models.ManyToManyField(User, related_name='subscriptions', verbose_name=_('Subscribers'), blank=True)
108
post_count = models.IntegerField(_('Post count'), blank=True, default=0)
111
ordering = ['-created']
112
verbose_name = _('Topic')
113
verbose_name_plural = _('Topics')
115
def __unicode__(self):
120
return self.posts.all().order_by('created').select_related()[0]
124
return self.posts.all().order_by('-created').select_related()[0]
126
def get_absolute_url(self):
127
return reverse('pybb_topic', args=[self.id])
129
def save(self, *args, **kwargs):
131
self.created = datetime.now()
132
super(Topic, self).save(*args, **kwargs)
134
def update_read(self, user):
135
read, new = Read.objects.get_or_create(user=user, topic=self)
137
read.time = datetime.now()
140
#def has_unreads(self, user):
142
#read = Read.objects.get(user=user, topic=self)
143
#except Read.DoesNotExist:
146
#return self.updated > read.time
149
class RenderableItem(models.Model):
151
Base class for models that has markup, body, body_text and body_html fields.
158
if self.markup == 'bbcode':
159
self.body_html = mypostmarkup.markup(self.body, auto_urls=False)
160
elif self.markup == 'markdown':
161
self.body_html = unicode(Markdown(self.body, safe_mode='escape'))
163
raise Exception('Invalid markup property: %s' % self.markup)
165
# Remove tags which was generated with the markup processor
166
text = strip_tags(self.body_html)
168
# Unescape entities which was generated with the markup processor
169
self.body_text = unescape(text)
171
self.body_html = urlize(self.body_html)
174
class Post(RenderableItem):
175
topic = models.ForeignKey(Topic, related_name='posts', verbose_name=_('Topic'))
176
user = models.ForeignKey(User, related_name='posts', verbose_name=_('User'))
177
created = models.DateTimeField(_('Created'), blank=True)
178
updated = models.DateTimeField(_('Updated'), blank=True, null=True)
179
markup = models.CharField(_('Markup'), max_length=15, default=pybb_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
180
body = models.TextField(_('Message'))
181
body_html = models.TextField(_('HTML version'))
182
body_text = models.TextField(_('Text version'))
183
user_ip = models.IPAddressField(_('User IP'), blank=True, default='')
187
ordering = ['created']
188
verbose_name = _('Post')
189
verbose_name_plural = _('Posts')
193
tail = len(self.body) > LIMIT and '...' or ''
194
return self.body[:LIMIT] + tail
196
__unicode__ = summary
198
def save(self, *args, **kwargs):
199
if self.created is None:
200
self.created = datetime.now()
203
new = self.id is None
206
self.topic.updated = datetime.now()
207
self.topic.post_count += 1
209
self.topic.forum.updated = self.topic.updated
210
self.topic.forum.post_count += 1
211
self.topic.forum.save()
213
super(Post, self).save(*args, **kwargs)
216
def get_absolute_url(self):
217
return reverse('pybb_post', args=[self.id])
220
def delete(self, *args, **kwargs):
222
head_post_id = self.topic.posts.order_by('created')[0].id
223
super(Post, self).delete(*args, **kwargs)
225
self.topic.post_count -= 1
227
self.topic.forum.post_count -= 1
228
self.topic.forum.save()
230
if self_id == head_post_id:
234
class Profile(models.Model):
235
user = AutoOneToOneField(User, related_name='pybb_profile', verbose_name=_('User'))
236
site = models.URLField(_('Site'), verify_exists=False, blank=True, default='')
237
jabber = models.CharField(_('Jabber'), max_length=80, blank=True, default='')
238
icq = models.CharField(_('ICQ'), max_length=12, blank=True, default='')
239
msn = models.CharField(_('MSN'), max_length=80, blank=True, default='')
240
aim = models.CharField(_('AIM'), max_length=80, blank=True, default='')
241
yahoo = models.CharField(_('Yahoo'), max_length=80, blank=True, default='')
242
location = models.CharField(_('Location'), max_length=30, blank=True, default='')
243
signature = models.TextField(_('Signature'), blank=True, default='', max_length=pybb_settings.SIGNATURE_MAX_LENGTH)
244
time_zone = models.FloatField(_('Time zone'), choices=TZ_CHOICES, default=float(pybb_settings.DEFAULT_TIME_ZONE))
245
language = models.CharField(_('Language'), max_length=10, blank=True, default='',
246
choices=settings.LANGUAGES)
247
avatar = ExtendedImageField(_('Avatar'), blank=True, default='', upload_to=pybb_settings.AVATARS_UPLOAD_TO, width=pybb_settings.AVATAR_WIDTH, height=pybb_settings.AVATAR_HEIGHT)
248
show_signatures = models.BooleanField(_('Show signatures'), blank=True, default=True)
249
markup = models.CharField(_('Default markup'), max_length=15, default=pybb_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
252
verbose_name = _('Profile')
253
verbose_name_plural = _('Profiles')
257
def unread_pm_count(self):
258
return PrivateMessage.objects.filter(dst_user=self, read=False).count()
260
def post_count(self):
262
Return the nr of posts the user has. This uses djangos filter feature
263
will therefore hit the database. This should maybe be reworked when the
264
database grows to not be always calculated.
266
return Post.objects.filter(user=self.user).count()
268
class Read(models.Model):
270
For each topic that user has entered the time
271
is logged to this model.
274
user = models.ForeignKey(User, verbose_name=_('User'))
275
topic = models.ForeignKey(Topic, verbose_name=_('Topic'))
276
time = models.DateTimeField(_('Time'), blank=True)
279
unique_together = ['user', 'topic']
280
verbose_name = _('Read')
281
verbose_name_plural = _('Reads')
283
def save(self, *args, **kwargs):
284
if self.time is None:
285
self.time = datetime.now()
286
super(Read, self).save(*args, **kwargs)
289
def __unicode__(self):
290
return u'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, unicode(self.time))
293
class PrivateMessage(RenderableItem):
295
dst_user = models.ForeignKey(User, verbose_name=_('Recipient'), related_name='dst_users')
296
src_user = models.ForeignKey(User, verbose_name=_('Author'), related_name='src_users')
297
read = models.BooleanField(_('Read'), blank=True, default=False)
298
created = models.DateTimeField(_('Created'), blank=True)
299
markup = models.CharField(_('Markup'), max_length=15, default=pybb_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
300
subject = models.CharField(_('Subject'), max_length=255)
301
body = models.TextField(_('Message'))
302
body_html = models.TextField(_('HTML version'))
303
body_text = models.TextField(_('Text version'))
306
ordering = ['-created']
307
verbose_name = _('Private message')
308
verbose_name_plural = _('Private messages')
310
# TODO: summary and part of the save method is the same as in the Post model
311
# move to common functions
314
tail = len(self.body) > LIMIT and '...' or ''
315
return self.body[:LIMIT] + tail
317
def __unicode__(self):
320
def save(self, *args, **kwargs):
321
if self.created is None:
322
self.created = datetime.now()
325
new = self.id is None
326
super(PrivateMessage, self).save(*args, **kwargs)
328
def get_absolute_url(self):
329
return reverse('pybb_show_pm', args=[self.id])
332
class Attachment(models.Model):
333
post = models.ForeignKey(Post, verbose_name=_('Post'), related_name='attachments')
334
size = models.IntegerField(_('Size'))
335
content_type = models.CharField(_('Content type'), max_length=255)
336
path = models.CharField(_('Path'), max_length=255)
337
name = models.TextField(_('Name'))
338
hash = models.CharField(_('Hash'), max_length=40, blank=True, default='', db_index=True)
340
def save(self, *args, **kwargs):
341
super(Attachment, self).save(*args, **kwargs)
343
self.hash = sha.new(str(self.id) + settings.SECRET_KEY).hexdigest()
344
super(Attachment, self).save(*args, **kwargs)
346
def __unicode__(self):
349
def get_absolute_url(self):
350
return reverse('pybb_attachment', args=[self.hash])
352
def size_display(self):
356
elif size < 1024 * 1024:
357
return '%dKb' % int(size / 1024)
359
return '%.2fMb' % (size / float(1024 * 1024))
362
def get_absolute_path(self):
363
return os.path.join(settings.MEDIA_ROOT, pybb_settings.ATTACHMENT_UPLOAD_TO,
367
from pybb import signals
368
signals.setup_signals()