forummodel.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import urlparse
  2. import threading
  3. from mptt.managers import TreeManager
  4. from mptt.models import MPTTModel, TreeForeignKey
  5. from django.conf import settings
  6. from django.core.cache import cache
  7. from django.core.urlresolvers import reverse
  8. from django.db import models
  9. from django.utils.translation import ugettext_lazy as _
  10. from misago.signals import delete_forum_content, move_forum_content, rename_forum, rename_user
  11. thread_local = threading.local()
  12. class ForumManager(TreeManager):
  13. @property
  14. def forums_tree(self):
  15. try:
  16. return thread_local.misago_forums_tree
  17. except AttributeError:
  18. thread_local.misago_forums_tree = None
  19. return thread_local.misago_forums_tree
  20. @forums_tree.setter
  21. def forums_tree(self, value):
  22. thread_local.misago_forums_tree = value
  23. def special_pk(self, name):
  24. self.populate_tree()
  25. return self.forums_tree.get(name).pk
  26. def special_model(self, name):
  27. self.populate_tree()
  28. return self.forums_tree.get(name)
  29. def populate_tree(self, force=False):
  30. if not self.forums_tree:
  31. self.forums_tree = cache.get('forums_tree')
  32. if not self.forums_tree or force:
  33. self.forums_tree = {}
  34. for forum in Forum.objects.order_by('lft'):
  35. self.forums_tree[forum.pk] = forum
  36. if forum.special:
  37. self.forums_tree[forum.special] = forum
  38. cache.set('forums_tree', self.forums_tree)
  39. def forum_parents(self, forum, include_self=False):
  40. self.populate_tree()
  41. parents = []
  42. parent = self.forums_tree[forum]
  43. if include_self:
  44. parents.append(parent)
  45. while parent.level > 1:
  46. parent = self.forums_tree[parent.parent_id]
  47. parents.append(parent)
  48. result = []
  49. for i in reversed(parents):
  50. result.append(i)
  51. return list(result)
  52. def parents_aware_forum(self, forum):
  53. self.populate_tree()
  54. proxy = Forum()
  55. try:
  56. proxy.id = forum.pk
  57. proxy.pk = forum.pk
  58. except AttributeError:
  59. proxy.id = forum
  60. proxy.pk = forum
  61. proxy.closed = False
  62. for parent in self.forum_parents(proxy.pk):
  63. if parent.closed:
  64. proxy.closed = True
  65. return proxy
  66. return proxy
  67. def treelist(self, acl, parent=None, tracker=None):
  68. complete_list = []
  69. forums_list = []
  70. parents = {}
  71. if parent:
  72. queryset = Forum.objects.filter(pk__in=acl.known_forums).filter(lft__gt=parent.lft).filter(rght__lt=parent.rght).order_by('lft')
  73. else:
  74. queryset = Forum.objects.filter(pk__in=acl.known_forums).order_by('lft')
  75. for forum in queryset:
  76. forum.subforums = []
  77. forum.is_read = False
  78. if tracker:
  79. forum.is_read = tracker.is_read(forum)
  80. parents[forum.pk] = forum
  81. complete_list.append(forum)
  82. if forum.parent_id in parents:
  83. parents[forum.parent_id].subforums.append(forum)
  84. else:
  85. forums_list.append(forum)
  86. # Second iteration - sum up forum counters
  87. for forum in reversed(complete_list):
  88. if forum.parent_id in parents and parents[forum.parent_id].type != 'redirect':
  89. parents[forum.parent_id].threads += forum.threads
  90. parents[forum.parent_id].posts += forum.posts
  91. if acl.can_browse(forum.pk):
  92. # If forum is unread, make parent unread too
  93. if not forum.is_read:
  94. parents[forum.parent_id].is_read = False
  95. # Sum stats
  96. 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):
  97. parents[forum.parent_id].last_thread_id = forum.last_thread_id
  98. parents[forum.parent_id].last_thread_name = forum.last_thread_name
  99. parents[forum.parent_id].last_thread_slug = forum.last_thread_slug
  100. parents[forum.parent_id].last_thread_date = forum.last_thread_date
  101. parents[forum.parent_id].last_poster_id = forum.last_poster_id
  102. parents[forum.parent_id].last_poster_name = forum.last_poster_name
  103. parents[forum.parent_id].last_poster_slug = forum.last_poster_slug
  104. parents[forum.parent_id].last_poster_style = forum.last_poster_style
  105. return forums_list
  106. def ignored_users(self, user, forums):
  107. check_ids = []
  108. for forum in forums:
  109. forum.last_poster_ignored = False
  110. 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:
  111. check_ids.append(forum.last_poster_id)
  112. ignored_ids = []
  113. if check_ids and user.is_authenticated():
  114. for user in user.ignores.filter(id__in=check_ids).values('id'):
  115. ignored_ids.append(user['id'])
  116. def readable_forums(self, acl, include_special=False):
  117. self.populate_tree()
  118. readable = []
  119. for pk, forum in self.forums_tree.items():
  120. if ((include_special or not forum.special) and
  121. acl.forums.can_browse(forum.pk) and
  122. acl.threads.acl[forum.pk]['can_read_threads']):
  123. readable.append(forum.pk)
  124. return readable
  125. class Forum(MPTTModel):
  126. parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
  127. type = models.CharField(max_length=12)
  128. special = models.CharField(max_length=255, null=True, blank=True)
  129. name = models.CharField(max_length=255)
  130. slug = models.SlugField(max_length=255)
  131. description = models.TextField(null=True, blank=True)
  132. description_preparsed = models.TextField(null=True, blank=True)
  133. threads = models.PositiveIntegerField(default=0)
  134. threads_delta = models.PositiveIntegerField(default=0)
  135. posts = models.PositiveIntegerField(default=0)
  136. posts_delta = models.IntegerField(default=0)
  137. redirects = models.PositiveIntegerField(default=0)
  138. redirects_delta = models.IntegerField(default=0)
  139. last_thread = models.ForeignKey('Thread', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  140. last_thread_name = models.CharField(max_length=255, null=True, blank=True)
  141. last_thread_slug = models.SlugField(max_length=255, null=True, blank=True)
  142. last_thread_date = models.DateTimeField(null=True, blank=True)
  143. last_poster = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  144. last_poster_name = models.CharField(max_length=255, null=True, blank=True)
  145. last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
  146. last_poster_style = models.CharField(max_length=255, null=True, blank=True)
  147. prune_start = models.PositiveIntegerField(default=0)
  148. prune_last = models.PositiveIntegerField(default=0)
  149. pruned_archive = models.ForeignKey('self', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  150. redirect = models.CharField(max_length=255, null=True, blank=True)
  151. attrs = models.CharField(max_length=255, null=True, blank=True)
  152. show_details = models.BooleanField(default=True)
  153. style = models.CharField(max_length=255, null=True, blank=True)
  154. closed = models.BooleanField(default=False)
  155. objects = ForumManager()
  156. class Meta:
  157. app_label = 'misago'
  158. def save(self, *args, **kwargs):
  159. super(Forum, self).save(*args, **kwargs)
  160. cache.delete('forums_tree')
  161. def delete(self, *args, **kwargs):
  162. delete_forum_content.send(sender=self)
  163. super(Forum, self).delete(*args, **kwargs)
  164. cache.delete('forums_tree')
  165. def __unicode__(self):
  166. if self.special == 'private_threads':
  167. return unicode(_('Private Threads'))
  168. if self.special == 'reports':
  169. return unicode(_('Reports'))
  170. if self.special == 'root':
  171. return unicode(_('Root Category'))
  172. return unicode(self.name)
  173. def forum_url(self):
  174. if self.special == 'private_threads':
  175. reverse('private_threads')
  176. if self.special == 'reports':
  177. reverse('reports')
  178. return reverse('forum', kwargs={'forum': self.pk, 'slug': self.slug})
  179. def set_description(self, description):
  180. self.description = description.strip()
  181. self.description_preparsed = ''
  182. if self.description:
  183. import markdown
  184. self.description_preparsed = markdown.markdown(description, safe_mode='escape', output_format=settings.OUTPUT_FORMAT)
  185. def copy_permissions(self, target):
  186. if target.pk != self.pk:
  187. from misago.models import Role
  188. for role in Role.objects.all():
  189. perms = role.permissions
  190. try:
  191. perms['forums'][self.pk] = perms['forums'][target.pk]
  192. role.permissions = perms
  193. role.save(force_update=True)
  194. except KeyError:
  195. pass
  196. def move_content(self, target):
  197. move_forum_content.send(sender=self, move_to=target)
  198. def sync_name(self):
  199. rename_forum.send(sender=self)
  200. def attr(self, att):
  201. if self.attrs:
  202. return att in self.attrs.split()
  203. return False
  204. def redirect_domain(self):
  205. hostname = urlparse.urlparse(self.redirect).hostname
  206. scheme = urlparse.urlparse(self.redirect).scheme
  207. if scheme:
  208. scheme = '%s://' % scheme
  209. return '%s%s' % (scheme, hostname)
  210. def new_last_thread(self, thread):
  211. self.last_thread = thread
  212. self.last_thread_name = thread.name
  213. self.last_thread_slug = thread.slug
  214. self.last_thread_date = thread.last
  215. self.last_poster = thread.last_poster
  216. self.last_poster_name = thread.last_poster_name
  217. self.last_poster_slug = thread.last_poster_slug
  218. self.last_poster_style = thread.last_poster_style
  219. def sync(self):
  220. self.threads = self.thread_set.filter(moderated=False).filter(deleted=False).count()
  221. self.posts = self.post_set.filter(moderated=False).count()
  222. self.last_poster = None
  223. self.last_poster_name = None
  224. self.last_poster_slug = None
  225. self.last_poster_style = None
  226. self.last_thread = None
  227. self.last_thread_date = None
  228. self.last_thread_name = None
  229. self.last_thread_slug = None
  230. try:
  231. last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[0:][0]
  232. self.last_poster_name = last_thread.last_poster_name
  233. self.last_poster_slug = last_thread.last_poster_slug
  234. self.last_poster_style = last_thread.last_poster_style
  235. if last_thread.last_poster:
  236. self.last_poster = last_thread.last_poster
  237. self.last_thread = last_thread
  238. self.last_thread_date = last_thread.last
  239. self.last_thread_name = last_thread.name
  240. self.last_thread_slug = last_thread.slug
  241. except (IndexError, AttributeError):
  242. pass
  243. def prune(self):
  244. pass
  245. """
  246. Signals
  247. """
  248. def rename_user_handler(sender, **kwargs):
  249. Forum.objects.filter(last_poster=sender).update(
  250. last_poster_name=sender.username,
  251. last_poster_slug=sender.username_slug,
  252. )
  253. rename_user.connect(rename_user_handler, dispatch_uid='rename_forums_last_poster')