forummodel.py 12 KB

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