source: djangobb_forum/models.py @ 401:1df250e6e5e6

Last change on this file since 401:1df250e6e5e6 was 401:1df250e6e5e6, checked in by Igor Yanchenko <yanchenko.igor@…>, 16 months ago

stupid error fix

File size: 19.2 KB
Line 
1from datetime import datetime
2import os
3import os.path
4
5from django.db import models
6from django.contrib.auth.models import User, Group
7from django.conf import settings
8from django.utils.translation import ugettext_lazy as _
9from django.utils.hashcompat import sha_constructor
10from django.db.models.signals import post_save
11
12from djangobb_forum.fields import AutoOneToOneField, ExtendedImageField, JSONField
13from djangobb_forum.util import smiles, convert_text_to_html
14from djangobb_forum import settings as forum_settings
15
16if 'south' in settings.INSTALLED_APPS:
17    from south.modelsinspector import add_introspection_rules
18    add_introspection_rules([], ['^djangobb_forum\.fields\.AutoOneToOneField',
19                                 '^djangobb_forum\.fields\.JSONField',
20                                 '^djangobb_forum\.fields\.ExtendedImageField'])
21
22TZ_CHOICES = [(float(x[0]), x[1]) for x in (
23    (-12, '-12'), (-11, '-11'), (-10, '-10'), (-9.5, '-09.5'), (-9, '-09'),
24    (-8.5, '-08.5'), (-8, '-08 PST'), (-7, '-07 MST'), (-6, '-06 CST'),
25    (-5, '-05 EST'), (-4, '-04 AST'), (-3.5, '-03.5'), (-3, '-03 ADT'),
26    (-2, '-02'), (-1, '-01'), (0, '00 GMT'), (1, '+01 CET'), (2, '+02'),
27    (3, '+03'), (3.5, '+03.5'), (4, '+04'), (4.5, '+04.5'), (5, '+05'),
28    (5.5, '+05.5'), (6, '+06'), (6.5, '+06.5'), (7, '+07'), (8, '+08'),
29    (9, '+09'), (9.5, '+09.5'), (10, '+10'), (10.5, '+10.5'), (11, '+11'),
30    (11.5, '+11.5'), (12, '+12'), (13, '+13'), (14, '+14'),
31)]
32
33SIGN_CHOICES = (
34    (1, 'PLUS'),
35    (-1, 'MINUS'),
36)
37
38PRIVACY_CHOICES = (
39    (0, _(u'Display your e-mail address.')),
40    (1, _(u'Hide your e-mail address but allow form e-mail.')),
41    (2, _(u'Hide your e-mail address and disallow form e-mail.')),
42)
43
44MARKUP_CHOICES = [('bbcode', 'bbcode')]
45try:
46    import markdown
47    MARKUP_CHOICES.append(("markdown", "markdown"))
48except ImportError:
49    pass
50
51path = os.path.join(settings.STATIC_ROOT, 'djangobb_forum', 'themes')
52if os.path.exists(path):
53    # fix for collectstatic
54    THEME_CHOICES = [(theme, theme) for theme in os.listdir(path) 
55                     if os.path.isdir(os.path.join(path, theme))]
56else:
57    THEME_CHOICES = []
58
59class Category(models.Model):
60    name = models.CharField(_('Name'), max_length=80)
61    groups = models.ManyToManyField(Group,blank=True, null=True, verbose_name=_('Groups'), help_text=_('Only users from these groups can see this category'))
62    position = models.IntegerField(_('Position'), blank=True, default=0)
63
64    class Meta:
65        ordering = ['position']
66        verbose_name = _('Category')
67        verbose_name_plural = _('Categories')
68        permissions = (
69            ('add_category', 'can add new categories'),
70            ('del_category', 'can delete categories'),
71            ('edit_category', 'can edit categories'),
72            ('view_category', 'can see available categories'),
73            ('moderate_category', 'can moderate category'),
74        )
75
76    def __unicode__(self):
77        return self.name
78
79    def forum_count(self):
80        return self.forums.all().count()
81
82    @property
83    def topics(self):
84        return Topic.objects.filter(forum__category__id=self.id).select_related()
85
86    @property
87    def posts(self):
88        return Post.objects.filter(topic__forum__category__id=self.id).select_related()
89
90    def has_access(self, user):
91        if self.groups.exists():
92            if user.is_authenticated(): 
93                    if not self.groups.filter(user__pk=user.id).exists():
94                        return False
95            else:
96                return False
97        return True
98
99
100class Forum(models.Model):
101    category = models.ForeignKey(Category, related_name='forums', verbose_name=_('Category'))
102    name = models.CharField(_('Name'), max_length=80)
103    position = models.IntegerField(_('Position'), blank=True, default=0)
104    description = models.TextField(_('Description'), blank=True, default='')
105    moderators = models.ManyToManyField(User, blank=True, null=True, verbose_name=_('Moderators'))
106    updated = models.DateTimeField(_('Updated'), auto_now=True)
107    post_count = models.IntegerField(_('Post count'), blank=True, default=0)
108    topic_count = models.IntegerField(_('Topic count'), blank=True, default=0)
109    last_post = models.ForeignKey('Post', related_name='last_forum_post', blank=True, null=True)
110
111    class Meta:
112        ordering = ['position']
113        verbose_name = _('Forum')
114        verbose_name_plural = _('Forums')
115        permissions = (
116            ('add_forum', 'can add new forums'),
117            ('del_forum', 'can delete forums'),
118            ('edit_forum', 'can edit forums'),
119            ('view_forum', 'can see available forums'),
120            ('moderate_forum', 'can moderate forums'),
121        )
122
123    def __unicode__(self):
124        return self.name
125
126    @models.permalink
127    def get_absolute_url(self):
128        return ('djangobb:forum', [self.id])
129
130    @property
131    def posts(self):
132        return Post.objects.filter(topic__forum__id=self.id).select_related()
133
134
135class Topic(models.Model):
136    forum = models.ForeignKey(Forum, related_name='topics', verbose_name=_('Forum'))
137    name = models.CharField(_('Subject'), max_length=255)
138    created = models.DateTimeField(_('Created'), auto_now_add=True)
139    updated = models.DateTimeField(_('Updated'), null=True)
140    user = models.ForeignKey(User, verbose_name=_('User'))
141    views = models.IntegerField(_('Views count'), blank=True, default=0)
142    sticky = models.BooleanField(_('Sticky'), blank=True, default=False)
143    closed = models.BooleanField(_('Closed'), blank=True, default=False)
144    subscribers = models.ManyToManyField(User, related_name='subscriptions', verbose_name=_('Subscribers'), blank=True)
145    post_count = models.IntegerField(_('Post count'), blank=True, default=0)
146    last_post = models.ForeignKey('Post', related_name='last_topic_post', blank=True, null=True)
147
148    class Meta:
149        ordering = ['-updated']
150        get_latest_by = 'updated'
151        verbose_name = _('Topic')
152        verbose_name_plural = _('Topics')
153        permissions = (
154            ('add_topic', 'can add new topics'),
155            ('del_topic', 'can delete topics'),
156            ('edit_topic', 'can edit topics'),
157            ('move_topic', 'can move topics'),
158            ('close_topic', 'can close topics'),
159            ('sticky_topic', 'can sticky topics'),
160            ('view_topic', 'can see available topics'),
161            ('moderate_topic', 'can moderate topics'),
162        )
163
164    def __unicode__(self):
165        return self.name
166
167    def delete(self, *args, **kwargs):
168        try:
169            last_post = self.posts.latest()
170            last_post.last_forum_post.clear()
171        except Post.DoesNotExist:
172            pass
173        else:
174            last_post.last_forum_post.clear()
175        forum = self.forum
176        super(Topic, self).delete(*args, **kwargs)
177        try:
178            forum.last_post = Topic.objects.filter(forum__id=forum.id).latest().last_post
179        except Topic.DoesNotExist:
180            forum.last_post = None
181        forum.topic_count = Topic.objects.filter(forum__id=forum.id).count()
182        forum.post_count = Post.objects.filter(topic__forum__id=forum.id).count()
183        forum.save()
184
185    @property
186    def head(self):
187        try:
188            return self.posts.select_related().order_by('created')[0]
189        except IndexError:
190            return None
191
192    @property
193    def reply_count(self):
194        return self.post_count - 1
195
196    @models.permalink
197    def get_absolute_url(self):
198        return ('djangobb:topic', [self.id])
199
200    def update_read(self, user):
201        tracking = user.posttracking
202        #if last_read > last_read - don't check topics
203        if tracking.last_read and (tracking.last_read > self.last_post.created):
204            return
205        if isinstance(tracking.topics, dict):
206            #clear topics if len > 5Kb and set last_read to current time
207            if len(tracking.topics) > 5120:
208                tracking.topics = None
209                tracking.last_read = datetime.now()
210                tracking.save()
211            #update topics if exist new post or does't exist in dict
212            if self.last_post_id > tracking.topics.get(str(self.id), 0):
213                tracking.topics[str(self.id)] = self.last_post_id
214                tracking.save()
215        else:
216            #initialize topic tracking dict
217            tracking.topics = {self.id: self.last_post_id}
218            tracking.save()
219
220
221class Post(models.Model):
222    topic = models.ForeignKey(Topic, related_name='posts', verbose_name=_('Topic'))
223    user = models.ForeignKey(User, related_name='posts', verbose_name=_('User'))
224    created = models.DateTimeField(_('Created'), auto_now_add=True)
225    updated = models.DateTimeField(_('Updated'), blank=True, null=True)
226    updated_by = models.ForeignKey(User, verbose_name=_('Updated by'), blank=True, null=True)
227    markup = models.CharField(_('Markup'), max_length=15, default=forum_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
228    body = models.TextField(_('Message'))
229    body_html = models.TextField(_('HTML version'))
230    user_ip = models.IPAddressField(_('User IP'), blank=True, null=True)
231
232
233    class Meta:
234        ordering = ['created']
235        get_latest_by = 'created'
236        verbose_name = _('Post')
237        verbose_name_plural = _('Posts')
238        permissions = (
239            ('add_post', 'can add new posts'),
240            ('del_post', 'can remove posts'),
241            ('edit_post', 'can edit posts'),
242            ('view_post', 'can see available posts'),
243        )
244
245    def save(self, *args, **kwargs):
246        self.body_html = convert_text_to_html(self.body, self.markup) 
247        if forum_settings.SMILES_SUPPORT and self.user.forum_profile.show_smilies:
248            self.body_html = smiles(self.body_html)
249        super(Post, self).save(*args, **kwargs)
250
251
252    def delete(self, *args, **kwargs):
253        self_id = self.id
254        head_post_id = self.topic.posts.order_by('created')[0].id
255        forum = self.topic.forum
256        topic = self.topic
257        profile = self.user.forum_profile
258        self.last_topic_post.clear()
259        self.last_forum_post.clear()
260        super(Post, self).delete(*args, **kwargs)
261        #if post was last in topic - remove topic
262        if self_id == head_post_id:
263            topic.delete()
264        else:
265            try:
266                topic.last_post = Post.objects.filter(topic__id=topic.id).latest()
267            except Post.DoesNotExist:
268                topic.last_post = None
269            topic.post_count = Post.objects.filter(topic__id=topic.id).count()
270            topic.save()
271        try:
272            forum.last_post = Post.objects.filter(topic__forum__id=forum.id).latest()
273        except Post.DoesNotExist:
274            forum.last_post = None
275        #TODO: for speedup - save/update only changed fields
276        forum.post_count = Post.objects.filter(topic__forum__id=forum.id).count()
277        forum.topic_count = Topic.objects.filter(forum__id=forum.id).count()
278        forum.save()
279        profile.post_count = Post.objects.filter(user__id=self.user_id).count()
280        profile.save()
281
282    @models.permalink
283    def get_absolute_url(self):
284        return ('djangobb:post', [self.id])
285
286    def summary(self):
287        LIMIT = 50
288        tail = len(self.body) > LIMIT and '...' or ''
289        return self.body[:LIMIT] + tail
290
291    __unicode__ = summary
292
293
294class Reputation(models.Model):
295    from_user = models.ForeignKey(User, related_name='reputations_from', verbose_name=_('From'))
296    to_user = models.ForeignKey(User, related_name='reputations_to', verbose_name=_('To'))
297    post = models.ForeignKey(Post, related_name='post', verbose_name=_('Post'))
298    time = models.DateTimeField(_('Time'), auto_now_add=True)
299    sign = models.IntegerField(_('Sign'), choices=SIGN_CHOICES, default=0)
300    reason = models.TextField(_('Reason'), max_length=1000)
301
302    class Meta:
303        verbose_name = _('Reputation')
304        verbose_name_plural = _('Reputations')
305        unique_together = (('from_user', 'post'),)
306        permissions = (
307            ('add_vote', 'can vote'),
308            ('del_vote', 'can remove votes'),
309            ('edit_vote', 'can edit votes'),
310            ('view_vote', 'can see available votes'),
311        )
312
313    def __unicode__(self):
314        return u'T[%d], FU[%d], TU[%d]: %s' % (self.post.id, self.from_user.id, self.to_user.id, unicode(self.time))
315
316
317class ProfileManager(models.Manager):
318    def get_query_set(self):
319        qs = super(ProfileManager, self).get_query_set()
320        if forum_settings.REPUTATION_SUPPORT:
321            qs = qs.extra(select={'reply_total':'Select sum(sign) from djangobb_forum_reputation where to_user_id = djangobb_forum_profile.user_id group by to_user_id'})
322        return qs
323
324class Profile(models.Model):
325    user = AutoOneToOneField(User, related_name='forum_profile', verbose_name=_('User'))
326    status = models.CharField(_('Status'), max_length=30, blank=True)
327    site = models.URLField(_('Site'), verify_exists=False, blank=True)
328    jabber = models.CharField(_('Jabber'), max_length=80, blank=True)
329    icq = models.CharField(_('ICQ'), max_length=12, blank=True)
330    msn = models.CharField(_('MSN'), max_length=80, blank=True)
331    aim = models.CharField(_('AIM'), max_length=80, blank=True)
332    yahoo = models.CharField(_('Yahoo'), max_length=80, blank=True)
333    location = models.CharField(_('Location'), max_length=30, blank=True)
334    signature = models.TextField(_('Signature'), blank=True, default='', max_length=forum_settings.SIGNATURE_MAX_LENGTH)
335    signature_html = models.TextField(_('Signature'), blank=True, default='', max_length=forum_settings.SIGNATURE_MAX_LENGTH)
336    time_zone = models.FloatField(_('Time zone'), choices=TZ_CHOICES, default=float(forum_settings.DEFAULT_TIME_ZONE))
337    language = models.CharField(_('Language'), max_length=5, default='', choices=settings.LANGUAGES)
338    avatar = ExtendedImageField(_('Avatar'), blank=True, default='', upload_to=forum_settings.AVATARS_UPLOAD_TO, width=forum_settings.AVATAR_WIDTH, height=forum_settings.AVATAR_HEIGHT)
339    theme = models.CharField(_('Theme'), choices=THEME_CHOICES, max_length=80, default='default')
340    show_avatar = models.BooleanField(_('Show avatar'), blank=True, default=True)
341    show_signatures = models.BooleanField(_('Show signatures'), blank=True, default=True)
342    show_smilies = models.BooleanField(_('Show smilies'), blank=True, default=True)
343    privacy_permission = models.IntegerField(_('Privacy permission'), choices=PRIVACY_CHOICES, default=1)
344    markup = models.CharField(_('Default markup'), max_length=15, default=forum_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
345    post_count = models.IntegerField(_('Post count'), blank=True, default=0)
346
347    objects = ProfileManager()
348
349    class Meta:
350        verbose_name = _('Profile')
351        verbose_name_plural = _('Profiles')
352        permissions = (
353            ('add_profile', 'can add new profiles'),
354            ('del_profile', 'can remove profiles'),
355            ('edit_profile', 'can edit profiles'),
356            ('view_profile', 'can see available profiles'),
357        )
358
359    def last_post(self):
360        posts = Post.objects.filter(user__id=self.user_id).order_by('-created')
361        if posts:
362            return posts[0].created
363        else:
364            return  None
365
366    def reply_count_minus(self):
367        return Reputation.objects.filter(to_user__id=self.user_id, sign=-1).count()
368
369    def reply_count_plus(self):
370        return Reputation.objects.filter(to_user__id=self.user_id, sign=1).count()
371
372
373class PostTracking(models.Model):
374    """
375    Model for tracking read/unread posts.
376    In topics stored ids of topics and last_posts as dict.
377    """
378
379    user = AutoOneToOneField(User)
380    topics = JSONField(null=True)
381    last_read = models.DateTimeField(null=True)
382
383    class Meta:
384        verbose_name = _('Post tracking')
385        verbose_name_plural = _('Post tracking')
386        permissions = (
387            ('add_posttracking', 'can add new post tracking'),
388            ('del_posttracking', 'can remove post trackings'),
389            ('edit_posttracking', 'can edit posttrackings'),
390            ('view_posttracking', 'can see available posttrackings'),
391        )
392
393    def __unicode__(self):
394        return self.user.username
395
396
397class Report(models.Model):
398    reported_by = models.ForeignKey(User, related_name='reported_by', verbose_name=_('Reported by'))
399    post = models.ForeignKey(Post, verbose_name=_('Post'))
400    zapped = models.BooleanField(_('Zapped'), blank=True, default=False)
401    zapped_by = models.ForeignKey(User, related_name='zapped_by', blank=True, null=True,  verbose_name=_('Zapped by'))
402    created = models.DateTimeField(_('Created'), blank=True)
403    reason = models.TextField(_('Reason'), blank=True, default='', max_length='1000')
404
405    class Meta:
406        verbose_name = _('Report')
407        verbose_name_plural = _('Reports')
408        permissions = (
409            ('add_report', 'can send reports'),
410            ('del_report', 'can remove reports'),
411            ('edit_report', 'can edit reports'),
412            ('view_report', 'can see reports'),
413        )
414
415    def __unicode__(self):
416        return u'%s %s' % (self.reported_by ,self.zapped)
417
418class Ban(models.Model):
419    user = models.OneToOneField(User, verbose_name=_('Banned user'), related_name='ban_users')
420    ban_start = models.DateTimeField(_('Ban start'), default=datetime.now)
421    ban_end = models.DateTimeField(_('Ban end'), blank=True, null=True)
422    reason = models.TextField(_('Reason'))
423
424    class Meta:
425        verbose_name = _('Ban')
426        verbose_name_plural = _('Bans')
427        permissions = (
428            ('add_ban', 'can ban users'),
429            ('del_ban', 'can remove users ban'),
430            ('edit_ban', 'can edit users ban'),
431            ('view_ban', 'can see banned users'),
432        )
433
434    def __unicode__(self):
435        return self.user.username
436
437    def save(self, *args, **kwargs):
438        self.user.is_active = False
439        self.user.save()
440        super(Ban, self).save(*args, **kwargs)
441
442    def delete(self, *args, **kwargs):
443        self.user.is_active = True
444        self.user.save()
445        super(Ban, self).delete(*args, **kwargs)
446
447
448class Attachment(models.Model):
449    post = models.ForeignKey(Post, verbose_name=_('Post'), related_name='attachments')
450    size = models.IntegerField(_('Size'))
451    content_type = models.CharField(_('Content type'), max_length=255)
452    path = models.CharField(_('Path'), max_length=255)
453    name = models.TextField(_('Name'))
454    hash = models.CharField(_('Hash'), max_length=40, blank=True, default='', db_index=True)
455
456    class Meta:
457        permissions = (
458            ('add_attachment', 'can upload attachments'),
459            ('del_attachment', 'can remove attachments'),
460            ('edit_attachment', 'can edit attachments'),
461            ('view_attachment', 'can download attachments'),
462        )
463
464    def __unicode__(self):
465        return self.name
466
467    def save(self, *args, **kwargs):
468        super(Attachment, self).save(*args, **kwargs)
469        if not self.hash:
470            self.hash = sha_constructor(str(self.id) + settings.SECRET_KEY).hexdigest()
471        super(Attachment, self).save(*args, **kwargs)
472
473    @models.permalink
474    def get_absolute_url(self):
475        return ('djangobb:forum_attachment', [self.hash])
476
477    def get_absolute_path(self):
478        return os.path.join(settings.MEDIA_ROOT, forum_settings.ATTACHMENT_UPLOAD_TO,
479                            self.path)
480
481
482from .signals import post_saved, topic_saved
483
484post_save.connect(post_saved, sender=Post, dispatch_uid='djangobb_post_save')
485post_save.connect(topic_saved, sender=Topic, dispatch_uid='djangobb_topic_save')
Note: See TracBrowser for help on using the repository browser.