# coding: utf-8 from hashlib import sha1 import os from django.conf import settings from django.contrib.auth.models import User, Group from django.db import models from django.db.models import aggregates from django.db.models.signals import post_save from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from djangobb_forum.fields import AutoOneToOneField, ExtendedImageField, JSONField from djangobb_forum.util import smiles, convert_text_to_html from djangobb_forum import settings as forum_settings if 'south' in settings.INSTALLED_APPS: from south.modelsinspector import add_introspection_rules add_introspection_rules([], ['^djangobb_forum\.fields\.AutoOneToOneField', '^djangobb_forum\.fields\.JSONField', '^djangobb_forum\.fields\.ExtendedImageField']) TZ_CHOICES = [(float(x[0]), x[1]) for x in ( (-12, '-12'), (-11, '-11'), (-10, '-10'), (-9.5, '-09.5'), (-9, '-09'), (-8.5, '-08.5'), (-8, '-08 PST'), (-7, '-07 MST'), (-6, '-06 CST'), (-5, '-05 EST'), (-4, '-04 AST'), (-3.5, '-03.5'), (-3, '-03 ADT'), (-2, '-02'), (-1, '-01'), (0, '00 GMT'), (1, '+01 CET'), (2, '+02'), (3, '+03'), (3.5, '+03.5'), (4, '+04'), (4.5, '+04.5'), (5, '+05'), (5.5, '+05.5'), (6, '+06'), (6.5, '+06.5'), (7, '+07'), (8, '+08'), (9, '+09'), (9.5, '+09.5'), (10, '+10'), (10.5, '+10.5'), (11, '+11'), (11.5, '+11.5'), (12, '+12'), (13, '+13'), (14, '+14'), )] SIGN_CHOICES = ( (1, 'PLUS'), (-1, 'MINUS'), ) PRIVACY_CHOICES = ( (0, _(u'Display your e-mail address.')), (1, _(u'Hide your e-mail address but allow form e-mail.')), (2, _(u'Hide your e-mail address and disallow form e-mail.')), ) MARKUP_CHOICES = [('bbcode', 'bbcode')] try: import markdown MARKUP_CHOICES.append(("markdown", "markdown")) except ImportError: pass path = os.path.join(settings.STATIC_ROOT, 'djangobb_forum', 'themes') if os.path.exists(path): # fix for collectstatic THEME_CHOICES = [(theme, theme) for theme in os.listdir(path) if os.path.isdir(os.path.join(path, theme))] else: THEME_CHOICES = [] class Category(models.Model): name = models.CharField(_('Name'), max_length=80) groups = models.ManyToManyField(Group, blank=True, null=True, verbose_name=_('Groups'), help_text=_('Only users from these groups can see this category')) position = models.IntegerField(_('Position'), blank=True, default=0) class Meta: ordering = ['position'] verbose_name = _('Category') verbose_name_plural = _('Categories') def __unicode__(self): return self.name def forum_count(self): return self.forums.all().count() @property def topics(self): return Topic.objects.filter(forum__category__id=self.id).select_related() @property def posts(self): return Post.objects.filter(topic__forum__category__id=self.id).select_related() def has_access(self, user): if user.is_superuser: return True if self.groups.exists(): if user.is_authenticated(): if not self.groups.filter(user__pk=user.id).exists(): return False else: return False return True class Forum(models.Model): category = models.ForeignKey(Category, related_name='forums', verbose_name=_('Category')) moderator_only = models.BooleanField(_('New topics by moderators only'), default=False) name = models.CharField(_('Name'), max_length=80) position = models.IntegerField(_('Position'), blank=True, default=0) description = models.TextField(_('Description'), blank=True, default='') moderators = models.ManyToManyField(User, blank=True, null=True, verbose_name=_('Moderators')) updated = models.DateTimeField(_('Updated'), auto_now=True) post_count = models.IntegerField(_('Post count'), blank=True, default=0) topic_count = models.IntegerField(_('Topic count'), blank=True, default=0) last_post = models.ForeignKey('Post', related_name='last_forum_post', blank=True, null=True) class Meta: ordering = ['position'] verbose_name = _('Forum') verbose_name_plural = _('Forums') def __unicode__(self): return self.name @models.permalink def get_absolute_url(self): return ('djangobb:forum', [self.id]) @property def posts(self): return Post.objects.filter(topic__forum__id=self.id).select_related() class Topic(models.Model): forum = models.ForeignKey(Forum, related_name='topics', verbose_name=_('Forum')) name = models.CharField(_('Subject'), max_length=255) created = models.DateTimeField(_('Created'), auto_now_add=True) updated = models.DateTimeField(_('Updated'), null=True) user = models.ForeignKey(User, verbose_name=_('User')) views = models.IntegerField(_('Views count'), blank=True, default=0) sticky = models.BooleanField(_('Sticky'), blank=True, default=False) closed = models.BooleanField(_('Closed'), blank=True, default=False) subscribers = models.ManyToManyField(User, related_name='subscriptions', verbose_name=_('Subscribers'), blank=True) post_count = models.IntegerField(_('Post count'), blank=True, default=0) last_post = models.ForeignKey('Post', related_name='last_topic_post', blank=True, null=True) class Meta: ordering = ['-updated'] get_latest_by = 'updated' verbose_name = _('Topic') verbose_name_plural = _('Topics') permissions = ( ('delayed_close', 'Can close topics after an amount of time has passed'), ) def __unicode__(self): return self.name def delete(self, *args, **kwargs): try: last_post = self.posts.latest() last_post.last_forum_post.clear() except Post.DoesNotExist: pass else: last_post.last_forum_post.clear() forum = self.forum if forum_settings.SOFT_DELETE_TOPICS and (self.forum != get_object_or_404(Forum, pk=forum_settings.SOFT_DELETE_TOPICS) or kwargs.get('staff', False)): self.forum = get_object_or_404(Forum, pk=forum_settings.SOFT_DELETE_TOPICS) self.save() else: super(Topic, self).delete(*args, **kwargs) try: forum.last_post = Topic.objects.filter(forum__id=forum.id).latest().last_post except Topic.DoesNotExist: forum.last_post = None forum.topic_count = Topic.objects.filter(forum__id=forum.id).count() forum.post_count = Post.objects.filter(topic__forum__id=forum.id).count() forum.save() @property def head(self): try: return self.posts.select_related().order_by('created')[0] except IndexError: return None @property def reply_count(self): return self.post_count - 1 @models.permalink def get_absolute_url(self): return ('djangobb:topic', [self.id]) def update_read(self, user): tracking = user.posttracking #if last_read > last_read - don't check topics if tracking.last_read and (tracking.last_read > self.last_post.created): return if isinstance(tracking.topics, dict): #clear topics if len > 5Kb and set last_read to current time if len(tracking.topics) > 5120: tracking.topics = None tracking.last_read = timezone.now() tracking.save() #update topics if exist new post or does't exist in dict if self.last_post_id > tracking.topics.get(str(self.id), 0): tracking.topics[str(self.id)] = self.last_post_id tracking.save() else: #initialize topic tracking dict tracking.topics = {self.id: self.last_post_id} tracking.save() class Post(models.Model): topic = models.ForeignKey(Topic, related_name='posts', verbose_name=_('Topic')) user = models.ForeignKey(User, related_name='posts', verbose_name=_('User')) created = models.DateTimeField(_('Created'), auto_now_add=True) updated = models.DateTimeField(_('Updated'), blank=True, null=True) updated_by = models.ForeignKey(User, verbose_name=_('Updated by'), blank=True, null=True) markup = models.CharField(_('Markup'), max_length=15, default=forum_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES) body = models.TextField(_('Message')) body_html = models.TextField(_('HTML version')) user_ip = models.IPAddressField(_('User IP'), blank=True, null=True) class Meta: ordering = ['created'] get_latest_by = 'created' verbose_name = _('Post') verbose_name_plural = _('Posts') permissions = ( ('fast_post', 'Can add posts without a time limit'), ('med_post', 'Can add posts at medium speed'), ('post_external_links', 'Can post external links'), ) def save(self, *args, **kwargs): self.body_html = convert_text_to_html(self.body, self.user.forum_profile) if forum_settings.SMILES_SUPPORT and self.user.forum_profile.show_smilies: self.body_html = smiles(self.body_html) super(Post, self).save(*args, **kwargs) def delete(self, *args, **kwargs): self_id = self.id head_post_id = self.topic.posts.order_by('created')[0].id forum = self.topic.forum topic = self.topic profile = self.user.forum_profile self.last_topic_post.clear() self.last_forum_post.clear() # If we actually delete the post, we lose any reports that my have come from it. Also, there is no recovery (but I don't care about that as much right now) if self_id == head_post_id: topic.delete() else: if forum_settings.SOFT_DELETE_POSTS and (self.topic != get_object_or_404(Topic, pk=forum_settings.SOFT_DELETE_POSTS) or kwargs.get('staff', False)): self.topic = get_object_or_404(Topic, pk=forum_settings.SOFT_DELETE_POSTS) self.save() else: super(Post, self).delete(*args, **kwargs) #if post was last in topic - remove topic try: topic.last_post = Post.objects.filter(topic__id=topic.id).latest() except Post.DoesNotExist: topic.last_post = None topic.post_count = Post.objects.filter(topic__id=topic.id).count() topic.save() try: forum.last_post = Post.objects.filter(topic__forum__id=forum.id).latest() except Post.DoesNotExist: forum.last_post = None #TODO: for speedup - save/update only changed fields forum.post_count = Post.objects.filter(topic__forum__id=forum.id).count() forum.topic_count = Topic.objects.filter(forum__id=forum.id).count() forum.save() profile.post_count = Post.objects.filter(user__id=self.user_id).count() profile.save() @models.permalink def get_absolute_url(self): return ('djangobb:post', [self.id]) def summary(self): LIMIT = 50 tail = len(self.body) > LIMIT and '...' or '' return self.body[:LIMIT] + tail __unicode__ = summary class Reputation(models.Model): from_user = models.ForeignKey(User, related_name='reputations_from', verbose_name=_('From')) to_user = models.ForeignKey(User, related_name='reputations_to', verbose_name=_('To')) post = models.ForeignKey(Post, related_name='post', verbose_name=_('Post')) time = models.DateTimeField(_('Time'), auto_now_add=True) sign = models.IntegerField(_('Sign'), choices=SIGN_CHOICES, default=0) reason = models.TextField(_('Reason'), max_length=1000) class Meta: verbose_name = _('Reputation') verbose_name_plural = _('Reputations') unique_together = (('from_user', 'post'),) def __unicode__(self): return u'T[%d], FU[%d], TU[%d]: %s' % (self.post.id, self.from_user.id, self.to_user.id, unicode(self.time)) class ProfileManager(models.Manager): use_for_related_fields = True def get_query_set(self): qs = super(ProfileManager, self).get_query_set() if forum_settings.REPUTATION_SUPPORT: 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', 'reply_count_minus': "SELECT SUM(sign) FROM djangobb_forum_reputation WHERE to_user_id = djangobb_forum_profile.user_id AND sign = '-1' GROUP BY to_user_id", 'reply_count_plus': "SELECT SUM(sign) FROM djangobb_forum_reputation WHERE to_user_id = djangobb_forum_profile.user_id AND sign = '1' GROUP BY to_user_id", }) return qs class Profile(models.Model): user = AutoOneToOneField(User, related_name='forum_profile', verbose_name=_('User')) status = models.CharField(_('Status'), max_length=30, blank=True) site = models.URLField(_('Site'), verify_exists=False, blank=True) jabber = models.CharField(_('Jabber'), max_length=80, blank=True) icq = models.CharField(_('ICQ'), max_length=12, blank=True) msn = models.CharField(_('MSN'), max_length=80, blank=True) aim = models.CharField(_('AIM'), max_length=80, blank=True) yahoo = models.CharField(_('Yahoo'), max_length=80, blank=True) location = models.CharField(_('Location'), max_length=30, blank=True) signature = models.TextField(_('Signature'), blank=True, default='', max_length=forum_settings.SIGNATURE_MAX_LENGTH) signature_html = models.TextField(_('Signature'), blank=True, default='', max_length=forum_settings.SIGNATURE_MAX_LENGTH) time_zone = models.FloatField(_('Time zone'), choices=TZ_CHOICES, default=float(forum_settings.DEFAULT_TIME_ZONE)) language = models.CharField(_('Language'), max_length=5, default='', choices=settings.LANGUAGES) avatar = ExtendedImageField(_('Avatar'), blank=True, default='', upload_to=forum_settings.AVATARS_UPLOAD_TO, width=forum_settings.AVATAR_WIDTH, height=forum_settings.AVATAR_HEIGHT) theme = models.CharField(_('Theme'), choices=THEME_CHOICES, max_length=80, default='default') show_avatar = models.BooleanField(_('Show avatar'), blank=True, default=True) show_signatures = models.BooleanField(_('Show signatures'), blank=True, default=True) show_smilies = models.BooleanField(_('Show smilies'), blank=True, default=True) privacy_permission = models.IntegerField(_('Privacy permission'), choices=PRIVACY_CHOICES, default=1) auto_subscribe = models.BooleanField(_('Auto subscribe'), help_text=_("Auto subscribe all topics you have created or reply."), blank=True, default=False) markup = models.CharField(_('Default markup'), max_length=15, default=forum_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES) post_count = models.IntegerField(_('Post count'), blank=True, default=0) objects = ProfileManager() class Meta: verbose_name = _('Profile') verbose_name_plural = _('Profiles') def last_post(self): posts = Post.objects.filter(user__id=self.user_id).order_by('-created') if posts: return posts[0].created else: return None class PostTracking(models.Model): """ Model for tracking read/unread posts. In topics stored ids of topics and last_posts as dict. """ user = AutoOneToOneField(User) topics = JSONField(null=True, blank=True) last_read = models.DateTimeField(null=True, blank=True) class Meta: verbose_name = _('Post tracking') verbose_name_plural = _('Post tracking') def __unicode__(self): return self.user.username class Report(models.Model): reported_by = models.ForeignKey(User, related_name='reported_by', verbose_name=_('Reported by')) post = models.ForeignKey(Post, verbose_name=_('Post')) zapped = models.BooleanField(_('Zapped'), blank=True, default=False) zapped_by = models.ForeignKey(User, related_name='zapped_by', blank=True, null=True, verbose_name=_('Zapped by')) created = models.DateTimeField(_('Created'), blank=True) reason = models.TextField(_('Reason'), blank=True, default='', max_length='1000') class Meta: verbose_name = _('Report') verbose_name_plural = _('Reports') def __unicode__(self): return u'%s %s' % (self.reported_by , self.zapped) class Ban(models.Model): user = models.OneToOneField(User, verbose_name=_('Banned user'), related_name='ban_users') ban_start = models.DateTimeField(_('Ban start'), default=timezone.now) ban_end = models.DateTimeField(_('Ban end'), blank=True, null=True) reason = models.TextField(_('Reason')) class Meta: verbose_name = _('Ban') verbose_name_plural = _('Bans') def __unicode__(self): return self.user.username def save(self, *args, **kwargs): self.user.is_active = False self.user.save() super(Ban, self).save(*args, **kwargs) def delete(self, *args, **kwargs): self.user.is_active = True self.user.save() super(Ban, self).delete(*args, **kwargs) class Attachment(models.Model): post = models.ForeignKey(Post, verbose_name=_('Post'), related_name='attachments') size = models.IntegerField(_('Size')) content_type = models.CharField(_('Content type'), max_length=255) path = models.CharField(_('Path'), max_length=255) name = models.TextField(_('Name')) hash = models.CharField(_('Hash'), max_length=40, blank=True, default='', db_index=True) def __unicode__(self): return self.name def save(self, *args, **kwargs): super(Attachment, self).save(*args, **kwargs) if not self.hash: self.hash = sha1(str(self.id) + settings.SECRET_KEY).hexdigest() super(Attachment, self).save(*args, **kwargs) @models.permalink def get_absolute_url(self): return ('djangobb:forum_attachment', [self.hash]) def get_absolute_path(self): return os.path.join(settings.MEDIA_ROOT, forum_settings.ATTACHMENT_UPLOAD_TO, self.path) #------------------------------------------------------------------------------ class Poll(models.Model): topic = models.ForeignKey(Topic) question = models.CharField(max_length=200) choice_count = models.PositiveSmallIntegerField(default=1, help_text=_("How many choices are allowed simultaneously."), ) active = models.BooleanField(default=True, help_text=_("Can users vote to this poll or just see the result?"), ) deactivate_date = models.DateTimeField(null=True, blank=True, help_text=_("Point of time after this poll would be automatic deactivated"), ) users = models.ManyToManyField(User, blank=True, null=True, help_text=_("Users who has voted this poll."), ) def auto_deactivate(self): if self.active and self.deactivate_date: now = timezone.now() if now > self.deactivate_date: self.active = False self.save() def __unicode__(self): return self.question class PollChoice(models.Model): poll = models.ForeignKey(Poll, related_name="choices") choice = models.CharField(max_length=200) votes = models.IntegerField(default=0, editable=False) def percent(self): if not self.votes: return 0.0 result = PollChoice.objects.filter(poll=self.poll).aggregate(aggregates.Sum("votes")) votes_sum = result["votes__sum"] return float(self.votes) / votes_sum * 100 def __unicode__(self): return self.choice #------------------------------------------------------------------------------ from .signals import post_saved, topic_saved post_save.connect(post_saved, sender=Post, dispatch_uid='djangobb_post_save') post_save.connect(topic_saved, sender=Topic, dispatch_uid='djangobb_topic_save')