source: djangobb/djangobb_forum/models.py @ 222:4d85334d2acd

Last change on this file since 222:4d85334d2acd was 218:6ece2dad5c55, checked in by slav0nic <slav0nic0@…>, 3 years ago

fixed problem with topic update time

File size: 16.6 KB
Line 
1from datetime import datetime
2import os
3import os.path
4from markdown import Markdown
5
6from django.db import models
7from django.contrib.auth.models import User, Group
8from django.utils.html import escape, strip_tags
9from django.conf import settings
10from django.utils.translation import ugettext_lazy as _
11from django.utils.hashcompat import sha_constructor
12
13from djangobb_forum.markups import mypostmarkup
14from djangobb_forum.fields import AutoOneToOneField, ExtendedImageField, JSONField
15from djangobb_forum.util import urlize, smiles
16from djangobb_forum import settings as forum_settings
17
18TZ_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'),
27)]
28
29SIGN_CHOICES = (
30    (1, 'PLUS'),
31    (-1, 'MINUS'),
32)
33
34PRIVACY_CHOICES = (
35    (0, _(u'Display your e-mail address.')),
36    (1, _(u'Hide your e-mail address but allow form e-mail.')),
37    (2, _(u'Hide your e-mail address and disallow form e-mail.')),
38)
39
40MARKUP_CHOICES = (
41    ('bbcode', 'bbcode'),
42    ('markdown', 'markdown'),
43)
44
45path = settings.PROJECT_ROOT + settings.MEDIA_URL + 'forum/themes/'
46THEME_CHOICES = [(theme, theme) for theme in os.listdir(path) 
47                 if os.path.isdir(path + theme)]
48
49class Category(models.Model):
50    name = models.CharField(_('Name'), max_length=80)
51    groups = models.ManyToManyField(Group,blank=True, null=True, verbose_name=_('Groups'), help_text=_('Only users from these groups can see this category'))
52    position = models.IntegerField(_('Position'), blank=True, default=0)
53
54    class Meta:
55        ordering = ['position']
56        verbose_name = _('Category')
57        verbose_name_plural = _('Categories')
58
59    def __unicode__(self):
60        return self.name
61
62    def forum_count(self):
63        return self.forums.all().count()
64
65    @property
66    def topics(self):
67        return Topic.objects.filter(forum__category=self).select_related()
68
69    @property
70    def posts(self):
71        return Post.objects.filter(topic__forum__category=self).select_related()
72
73    def has_access(self, user):
74        if self.groups.count() > 0:
75            if user.is_authenticated(): 
76                try:
77                    self.groups.get(user__pk=user.id)
78                except Group.DoesNotExist:
79                    return False
80            else:
81                return False
82        return True
83
84
85class Forum(models.Model):
86    category = models.ForeignKey(Category, related_name='forums', verbose_name=_('Category'))
87    name = models.CharField(_('Name'), max_length=80)
88    position = models.IntegerField(_('Position'), blank=True, default=0)
89    description = models.TextField(_('Description'), blank=True, default='')
90    moderators = models.ManyToManyField(User, blank=True, null=True, verbose_name=_('Moderators'))
91    updated = models.DateTimeField(_('Updated'), auto_now=True)
92    post_count = models.IntegerField(_('Post count'), blank=True, default=0)
93    topic_count = models.IntegerField(_('Topic count'), blank=True, default=0)
94    last_post = models.ForeignKey('Post', related_name='last_forum_post', blank=True, null=True)
95
96    class Meta:
97        ordering = ['position']
98        verbose_name = _('Forum')
99        verbose_name_plural = _('Forums')
100
101    def __unicode__(self):
102        return self.name
103
104    @models.permalink
105    def get_absolute_url(self):
106        return ('djangobb:forum', [self.id])
107
108    @property
109    def posts(self):
110        return Post.objects.filter(topic__forum=self).select_related()
111
112
113class Topic(models.Model):
114    forum = models.ForeignKey(Forum, related_name='topics', verbose_name=_('Forum'))
115    name = models.CharField(_('Subject'), max_length=255)
116    created = models.DateTimeField(_('Created'), auto_now_add=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(User, related_name='subscriptions', verbose_name=_('Subscribers'), blank=True)
123    post_count = models.IntegerField(_('Post count'), blank=True, default=0)
124    last_post = models.ForeignKey('Post', related_name='last_topic_post', blank=True, null=True)
125
126    class Meta:
127        ordering = ['-updated']
128        verbose_name = _('Topic')
129        verbose_name_plural = _('Topics')
130
131    def __unicode__(self):
132        return self.name
133
134    @property
135    def head(self):
136        try:
137            return self.posts.select_related().order_by('created')[0]
138        except IndexError:
139            return None
140
141    @property
142    def reply_count(self):
143        return self.post_count - 1
144
145    @models.permalink
146    def get_absolute_url(self):
147        return ('djangobb:topic', [self.id])
148
149    def update_read(self, user):
150        tracking = user.posttracking
151        #if last_read > last_read - don't check topics
152        if tracking.last_read and (tracking.last_read > self.last_post.created):
153            return
154        if isinstance(tracking.topics, dict):
155            #clear topics if len > 5Kb and set last_read to current time
156            if len(tracking.topics) > 5120:
157                tracking.topics = None
158                tracking.last_read = datetime.now()
159                tracking.save()
160            #update topics if exist new post or does't exist in dict
161            if self.last_post.id > tracking.topics.get(str(self.id), 0):
162                tracking.topics[str(self.id)] = self.last_post.id
163                tracking.save()
164        else:
165            #initialize topic tracking dict
166            tracking.topics = {self.id: self.last_post.id}
167            tracking.save()
168
169
170class Post(models.Model):
171    topic = models.ForeignKey(Topic, related_name='posts', verbose_name=_('Topic'))
172    user = models.ForeignKey(User, related_name='posts', verbose_name=_('User'))
173    created = models.DateTimeField(_('Created'), auto_now_add=True)
174    updated = models.DateTimeField(_('Updated'), blank=True, null=True)
175    markup = models.CharField(_('Markup'), max_length=15, default=forum_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
176    body = models.TextField(_('Message'))
177    body_html = models.TextField(_('HTML version'))
178    body_text = models.TextField(_('Text version'))
179    user_ip = models.IPAddressField(_('User IP'), blank=True, null=True)
180
181
182    class Meta:
183        ordering = ['created']
184        get_latest_by = 'created'
185        verbose_name = _('Post')
186        verbose_name_plural = _('Posts')
187
188    def save(self, *args, **kwargs):
189        if self.markup == 'bbcode':
190            self.body_html = mypostmarkup.markup(self.body, auto_urls=False)
191        elif self.markup == 'markdown':
192            self.body_html = unicode(Markdown(self.body, safe_mode='escape'))
193            #self.body_html = markdown(self.body, 'safe')
194        else:
195            raise Exception('Invalid markup property: %s' % self.markup)
196        self.body_text = strip_tags(self.body_html)
197        self.body_html = urlize(self.body_html)
198        if forum_settings.SMILES_SUPPORT:
199            self.body_html = smiles(self.body_html)
200        super(Post, self).save(*args, **kwargs)
201
202    def delete(self, *args, **kwargs):
203        self_id = self.id
204        head_post_id = self.topic.posts.order_by('created')[0].id
205        forum = self.topic.forum
206        topic = self.topic
207        self.last_topic_post.clear()
208        self.last_forum_post.clear()
209        super(Post, self).delete(*args, **kwargs)
210        #if post was last in topic - remove topic
211        if self_id == head_post_id:
212            topic.delete()
213        else:
214            try:
215                topic.last_post = Post.objects.filter(topic=topic).latest()
216            except Post.DoesNotExist:
217                topic.last_post = None
218            topic.post_count = Post.objects.filter(topic=topic).count()
219            topic.save()
220        try:
221            forum.last_post = Post.objects.filter(topic__forum=forum).latest()
222        except Post.DoesNotExist:
223            forum.last_post = None
224        forum.post_count = Post.objects.filter(topic__forum=forum).count()
225        forum.topic_count = Topic.objects.filter(forum=forum).count()
226        forum.save()
227
228    @models.permalink
229    def get_absolute_url(self):
230        return ('djangobb:post', [self.id])
231
232    def summary(self):
233        LIMIT = 50
234        tail = len(self.body) > LIMIT and '...' or '' 
235        return self.body[:LIMIT] + tail
236
237    __unicode__ = summary
238
239
240class Reputation(models.Model):
241    from_user = models.ForeignKey(User, related_name='reputations_from', verbose_name=_('From'))
242    to_user = models.ForeignKey(User, related_name='reputations_to', verbose_name=_('To'))
243    topic = models.ForeignKey(Topic, related_name='topic', verbose_name=_('Topic'))
244    time = models.DateTimeField(_('Time'), blank=True)
245    sign = models.IntegerField(_('Sign'), choices=SIGN_CHOICES, default=0)
246    reason = models.TextField(_('Reason'), blank=True, default='', max_length=1000)
247
248    class Meta:
249        verbose_name = _('Reputation')
250        verbose_name_plural = _('Reputations')
251
252    def __unicode__(self):
253        return u'T[%d], FU[%d], TU[%d]: %s' % (self.topic.id, self.from_user.id, self.to_user.id, unicode(self.time))
254
255
256class Profile(models.Model):
257    user = AutoOneToOneField(User, related_name='forum_profile', verbose_name=_('User'))
258    status = models.CharField(_('Status'), max_length=30, blank=True)
259    site = models.URLField(_('Site'), verify_exists=False, blank=True)
260    jabber = models.CharField(_('Jabber'), max_length=80, blank=True)
261    icq = models.CharField(_('ICQ'), max_length=12, blank=True)
262    msn = models.CharField(_('MSN'), max_length=80, blank=True)
263    aim = models.CharField(_('AIM'), max_length=80, blank=True)
264    yahoo = models.CharField(_('Yahoo'), max_length=80, blank=True)
265    location = models.CharField(_('Location'), max_length=30, blank=True)
266    signature = models.TextField(_('Signature'), blank=True, default='', max_length=forum_settings.SIGNATURE_MAX_LENGTH)
267    time_zone = models.FloatField(_('Time zone'), choices=TZ_CHOICES, default=float(forum_settings.DEFAULT_TIME_ZONE))
268    language = models.CharField(_('Language'), max_length=3, default='', choices=settings.LANGUAGES)
269    avatar = ExtendedImageField(_('Avatar'), blank=True, default='', upload_to=forum_settings.AVATARS_UPLOAD_TO, width=forum_settings.AVATAR_WIDTH, height=forum_settings.AVATAR_HEIGHT)
270    theme = models.CharField(_('Theme'), choices=THEME_CHOICES, max_length=80, default='default')
271    show_avatar = models.BooleanField(_('Show avatar'), blank=True, default=True)
272    show_signatures = models.BooleanField(_('Show signatures'), blank=True, default=True)
273    privacy_permission = models.IntegerField(_('Privacy permission'), choices=PRIVACY_CHOICES, default=1)
274    markup = models.CharField(_('Default markup'), max_length=15, default=forum_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
275    post_count = models.IntegerField(_('Post count'), blank=True, default=0)
276
277    class Meta:
278        verbose_name = _('Profile')
279        verbose_name_plural = _('Profiles')
280
281    def last_post(self):
282        posts = Post.objects.filter(user=self.user).order_by('-created')
283        if posts:
284            return posts[0].created
285        else:
286            return  None
287
288    def reply_count_minus(self):
289        return Reputation.objects.filter(to_user=self.user, sign=-1).count()
290
291    def reply_count_plus(self):
292        return Reputation.objects.filter(to_user=self.user, sign=1).count()
293
294
295class PostTracking(models.Model):
296    """
297    Model for tracking read/unread posts.
298    In topics stored ids of topics and last_posts as dict.
299    """
300
301    user = AutoOneToOneField(User)
302    topics = JSONField(null=True)
303    last_read = models.DateTimeField(null=True)
304
305    class Meta:
306        verbose_name = _('Post tracking')
307        verbose_name_plural = _('Post tracking')
308
309    def __unicode__(self):
310        return self.user.username
311
312
313class Report(models.Model):
314    reported_by = models.ForeignKey(User, related_name='reported_by', verbose_name=_('Reported by'))
315    post = models.ForeignKey(Post, verbose_name=_('Post'))
316    zapped = models.BooleanField(_('Zapped'), blank=True, default=False)
317    zapped_by = models.ForeignKey(User, related_name='zapped_by', blank=True, null=True,  verbose_name=_('Zapped by'))
318    created = models.DateTimeField(_('Created'), blank=True)
319    reason = models.TextField(_('Reason'), blank=True, default='', max_length='1000')
320
321    class Meta:
322        verbose_name = _('Report')
323        verbose_name_plural = _('Reports')
324
325    def __unicode__(self):
326        return u'%s %s' % (self.reported_by ,self.zapped)
327
328
329class PrivateMessage(models.Model):
330    dst_user = models.ForeignKey(User, verbose_name=_('Recipient'), related_name='dst_users')
331    src_user = models.ForeignKey(User, verbose_name=_('Author'), related_name='src_users')
332    read = models.BooleanField(_('Read'), blank=True, default=False)
333    created = models.DateTimeField(_('Created'), auto_now_add=True)
334    markup = models.CharField(_('Markup'), max_length=15, default=forum_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
335    subject = models.CharField(_('Subject'), max_length=255)
336    body = models.TextField(_('Message'))
337    body_html = models.TextField(_('HTML version'))
338    body_text = models.TextField(_('Text version'))
339
340    class Meta:
341        ordering = ['-created']
342        verbose_name = _('Private message')
343        verbose_name_plural = _('Private messages')
344
345    def __unicode__(self):
346        return self.subject
347
348    def save(self, *args, **kwargs):
349        if self.markup == 'bbcode':
350            self.body_html = mypostmarkup.markup(self.body, auto_urls=False)
351        elif self.markup == 'markdown':
352            self.body_html = unicode(Markdown(self.body, safe_mode='escape'))
353            #self.body_html = markdown(self.body, 'safe')
354        else:
355            raise Exception('Invalid markup property: %s' % self.markup)
356        self.body_text = strip_tags(self.body_html)
357        self.body_html = urlize(self.body_html)
358        if forum_settings.SMILES_SUPPORT:
359            self.body_html = smiles(self.body_html)
360        new = self.id is None
361        super(PrivateMessage, self).save(*args, **kwargs)
362
363    @models.permalink
364    def get_absolute_url(self):
365        return  ('djangobb:forum_show_pm', [self.id])
366
367    # TODO: summary and part of the save method is the same as in the Post model
368    # move to common functions
369    def summary(self):
370        LIMIT = 50
371        tail = len(self.body) > LIMIT and '...' or '' 
372        return self.body[:LIMIT] + tail
373
374
375class Ban(models.Model):
376    user = models.OneToOneField(User, verbose_name=_('Banned user'), related_name='ban_users')
377    ban_start = models.DateTimeField(_('Ban start'), default=datetime.now)
378    ban_end = models.DateTimeField(_('Ban end'), blank=True, null=True)
379    reason = models.TextField(_('Reason'))
380
381    class Meta:
382        verbose_name = _('Ban')
383        verbose_name_plural = _('Bans')
384
385    def __unicode__(self):
386        return self.user.username
387
388    def save(self, *args, **kwargs):
389        self.user.is_active = False
390        self.user.save()
391        super(Ban, self).save(*args, **kwargs)
392
393    def delete(self, *args, **kwargs):
394        self.user.is_active = True
395        self.user.save()
396        super(Ban, self).delete(*args, **kwargs)
397
398
399class Attachment(models.Model):
400    post = models.ForeignKey(Post, verbose_name=_('Post'), related_name='attachments')
401    size = models.IntegerField(_('Size'))
402    content_type = models.CharField(_('Content type'), max_length=255)
403    path = models.CharField(_('Path'), max_length=255)
404    name = models.TextField(_('Name'))
405    hash = models.CharField(_('Hash'), max_length=40, blank=True, default='', db_index=True)
406
407    def __unicode__(self):
408        return self.name
409
410    def save(self, *args, **kwargs):
411        super(Attachment, self).save(*args, **kwargs)
412        if not self.hash:
413            self.hash = sha_constructor(str(self.id) + settings.SECRET_KEY).hexdigest()
414        super(Attachment, self).save(*args, **kwargs)
415
416    @models.permalink
417    def get_absolute_url(self):
418        return ('djangobb:forum_attachment', [self.hash])
419
420    def get_absolute_path(self):
421        return os.path.join(settings.MEDIA_ROOT, forum_settings.ATTACHMENT_UPLOAD_TO,
422                            self.path)
Note: See TracBrowser for help on using the repository browser.