import urlparse import threading from mptt.managers import TreeManager from mptt.models import MPTTModel, TreeForeignKey from django.conf import settings from django.core.cache import cache from django.core.urlresolvers import reverse from django.db import models from django.db.models import Sum from django.utils.translation import ugettext_lazy as _ from misago.signals import delete_forum_content, move_forum_content, rename_forum, rename_user _thread_local = threading.local() class ForumManager(TreeManager): @property def forums_tree(self): try: return _thread_local.misago_forums_tree except AttributeError: _thread_local.misago_forums_tree = None return _thread_local.misago_forums_tree @forums_tree.setter def forums_tree(self, value): _thread_local.misago_forums_tree = value def special_pk(self, name): self.populate_tree() return self.forums_tree.get(name).pk def special_model(self, name): self.populate_tree() return self.forums_tree.get(name) def populate_tree(self, force=False): if not self.forums_tree: self.forums_tree = cache.get('forums_tree') if not self.forums_tree or force: self.forums_tree = {} for forum in Forum.objects.order_by('lft'): self.forums_tree[forum.pk] = forum if forum.special: self.forums_tree[forum.special] = forum cache.set('forums_tree', self.forums_tree) def forum_parents(self, forum, include_self=False): self.populate_tree() parents = [] parent = self.forums_tree[forum] if include_self: parents.append(parent) while parent.level > 1: parent = self.forums_tree[parent.parent_id] parents.append(parent) result = [] for i in reversed(parents): result.append(i) return list(result) def parents_aware_forum(self, forum): self.populate_tree() proxy = Forum() try: proxy.id = forum.pk proxy.pk = forum.pk except AttributeError: proxy.id = forum proxy.pk = forum proxy.closed = False for parent in self.forum_parents(proxy.pk): if parent.closed: proxy.closed = True return proxy return proxy def treelist(self, acl, parent=None, tracker=None): complete_list = [] forums_list = [] parents = {} if parent: queryset = Forum.objects.filter(pk__in=acl.known_forums).filter(lft__gt=parent.lft).filter(rght__lt=parent.rght).order_by('lft') else: queryset = Forum.objects.filter(pk__in=acl.known_forums).order_by('lft') for forum in queryset: forum.subforums = [] forum.is_read = False if tracker: forum.is_read = tracker.is_read(forum) parents[forum.pk] = forum complete_list.append(forum) if forum.parent_id in parents: parents[forum.parent_id].subforums.append(forum) else: forums_list.append(forum) # Second iteration - sum up forum counters for forum in reversed(complete_list): if forum.parent_id in parents and parents[forum.parent_id].type != 'redirect': parents[forum.parent_id].threads += forum.threads parents[forum.parent_id].posts += forum.posts if acl.can_browse(forum.pk): # If forum is unread, make parent unread too if not forum.is_read: parents[forum.parent_id].is_read = False # Sum stats if forum.last_thread_date and (not parents[forum.parent_id].last_thread_date or forum.last_thread_date > parents[forum.parent_id].last_thread_date): parents[forum.parent_id].last_thread_id = forum.last_thread_id parents[forum.parent_id].last_thread_name = forum.last_thread_name parents[forum.parent_id].last_thread_slug = forum.last_thread_slug parents[forum.parent_id].last_thread_date = forum.last_thread_date parents[forum.parent_id].last_poster_id = forum.last_poster_id parents[forum.parent_id].last_poster_name = forum.last_poster_name parents[forum.parent_id].last_poster_slug = forum.last_poster_slug parents[forum.parent_id].last_poster_style = forum.last_poster_style return forums_list def ignored_users(self, user, forums): check_ids = [] for forum in forums: forum.last_poster_ignored = False if user.is_authenticated() and user.pk != forum.last_poster_id and forum.last_poster_id and not forum.last_poster_id in check_ids: check_ids.append(forum.last_poster_id) ignored_ids = [] if check_ids and user.is_authenticated(): for user in user.ignores.filter(id__in=check_ids).values('id'): ignored_ids.append(user['id']) def readable_forums(self, acl, include_special=False): self.populate_tree() readable = [] for pk, forum in self.forums_tree.items(): if ((include_special or not forum.special) and acl.forums.can_browse(forum.pk) and acl.threads.acl[forum.pk]['can_read_threads'] == 2): readable.append(forum.pk) return readable def starter_readable_forums(self, acl): self.populate_tree() readable = [] for pk, forum in self.forums_tree.items(): if (not forum.special and acl.forums.can_browse(forum.pk) and acl.threads.acl[forum.pk]['can_read_threads'] == 1): readable.append(forum.pk) return readable def forum_by_name(self, forum, acl): forums = self.readable_forums(acl, True) forum = forum.lower() for f in forums: f = self.forums_tree[f] if forum == unicode(f).lower(): return f forum_len = len(forum) for f in forums: f = self.forums_tree[f] name = unicode(f).lower() if forum == unicode(f).lower()[0:forum_len]: return f return None class Forum(MPTTModel): parent = TreeForeignKey('self', null=True, blank=True, related_name='children') type = models.CharField(max_length=12) special = models.CharField(max_length=255, null=True, blank=True) name = models.CharField(max_length=255) slug = models.SlugField(max_length=255) description = models.TextField(null=True, blank=True) description_preparsed = models.TextField(null=True, blank=True) threads = models.PositiveIntegerField(default=0) threads_delta = models.PositiveIntegerField(default=0) posts = models.PositiveIntegerField(default=0) posts_delta = models.IntegerField(default=0) redirects = models.PositiveIntegerField(default=0) redirects_delta = models.IntegerField(default=0) last_thread = models.ForeignKey('Thread', related_name='+', null=True, blank=True, on_delete=models.SET_NULL) last_thread_name = models.CharField(max_length=255, null=True, blank=True) last_thread_slug = models.SlugField(max_length=255, null=True, blank=True) last_thread_date = models.DateTimeField(null=True, blank=True) last_poster = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL) last_poster_name = models.CharField(max_length=255, null=True, blank=True) last_poster_slug = models.SlugField(max_length=255, null=True, blank=True) last_poster_style = models.CharField(max_length=255, null=True, blank=True) prune_start = models.PositiveIntegerField(default=0) prune_last = models.PositiveIntegerField(default=0) pruned_archive = models.ForeignKey('self', related_name='+', null=True, blank=True, on_delete=models.SET_NULL) redirect = models.CharField(max_length=255, null=True, blank=True) attrs = models.CharField(max_length=255, null=True, blank=True) show_details = models.BooleanField(default=True) style = models.CharField(max_length=255, null=True, blank=True) closed = models.BooleanField(default=False) objects = ForumManager() class Meta: app_label = 'misago' def save(self, *args, **kwargs): super(Forum, self).save(*args, **kwargs) cache.delete('forums_tree') def delete(self, *args, **kwargs): delete_forum_content.send(sender=self) super(Forum, self).delete(*args, **kwargs) cache.delete('forums_tree') def __unicode__(self): if self.special == 'private_threads': return unicode(_('Private Threads')) if self.special == 'reports': return unicode(_('Reports')) if self.special == 'root': return unicode(_('Root Category')) return unicode(self.name) @property def url(self): if self.special == 'private_threads': reverse('private_threads') if self.special == 'reports': reverse('reports') if self.type == 'category': return reverse('category', kwargs={'forum': self.pk, 'slug': self.slug}) if self.type == 'redirect': return reverse('redirect', kwargs={'forum': self.pk, 'slug': self.slug}) return reverse('forum', kwargs={'forum': self.pk, 'slug': self.slug}) def thread_link(self, extra): if self.special == 'private_threads': route_prefix = 'private_thread' if self.special == 'reports': route_prefix = 'report' else: route_prefix = 'thread' if extra: return '%s_%s' % (route_prefix, extra) if extra else route_prefix return route_prefix def thread_url(self, thread, route=None): route_prefix = 'thread' if self.special: route_prefix = self.special[0:-1] link = '%s_%s' % (route_prefix, route) if route else route_prefix return reverse(link, kwargs={'thread': thread.pk, 'slug': thread.slug}) def set_description(self, description): self.description = description.strip() self.description_preparsed = '' if self.description: import markdown self.description_preparsed = markdown.markdown(description, safe_mode='escape', output_format=settings.OUTPUT_FORMAT) def copy_permissions(self, target): if target.pk != self.pk: from misago.models import Role for role in Role.objects.all(): perms = role.permissions try: perms['forums'][self.pk] = perms['forums'][target.pk] role.permissions = perms role.save(force_update=True) except KeyError: pass def move_content(self, target): move_forum_content.send(sender=self, move_to=target) def sync_name(self): rename_forum.send(sender=self) def attr(self, att): if self.attrs: return att in self.attrs.split() return False def redirect_domain(self): hostname = urlparse.urlparse(self.redirect).hostname scheme = urlparse.urlparse(self.redirect).scheme if scheme: scheme = '%s://' % scheme return '%s%s' % (scheme, hostname) def new_last_thread(self, thread): self.last_thread = thread self.last_thread_name = thread.name self.last_thread_slug = thread.slug self.last_thread_date = thread.last self.last_poster = thread.last_poster self.last_poster_name = thread.last_poster_name self.last_poster_slug = thread.last_poster_slug self.last_poster_style = thread.last_poster_style def sync_last(self): self.last_poster = None self.last_poster_name = None self.last_poster_slug = None self.last_poster_style = None self.last_thread = None self.last_thread_date = None self.last_thread_name = None self.last_thread_slug = None try: last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[:1][0] self.last_poster_name = last_thread.last_poster_name self.last_poster_slug = last_thread.last_poster_slug self.last_poster_style = last_thread.last_poster_style if last_thread.last_poster: self.last_poster = last_thread.last_poster self.last_thread = last_thread self.last_thread_date = last_thread.last self.last_thread_name = last_thread.name self.last_thread_slug = last_thread.slug except (IndexError, AttributeError): pass def sync(self): threads_qs = self.thread_set.filter(moderated=False).filter(deleted=False) self.posts = self.threads = threads_qs.count() replies = threads_qs.aggregate(Sum('replies')) if replies['replies__sum']: self.posts += replies['replies__sum'] self.sync_last() def prune(self): pass """ Signals """ def rename_user_handler(sender, **kwargs): Forum.objects.filter(last_poster=sender).update( last_poster_name=sender.username, last_poster_slug=sender.username_slug, ) rename_user.connect(rename_user_handler, dispatch_uid='rename_forums_last_poster')