forummodel.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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. @property
  174. def url(self):
  175. if self.special == 'private_threads':
  176. reverse('private_threads')
  177. if self.special == 'reports':
  178. reverse('reports')
  179. return reverse('forum', kwargs={'forum': self.pk, 'slug': self.slug})
  180. def thread_url(self, thread, route=None):
  181. route_prefix = 'thread'
  182. if self.special:
  183. route_prefix = self.special[0:-1]
  184. link = '%s_%s' % (route_prefix, route) if route else route_prefix
  185. return reverse(link, kwargs={'thread': thread.pk, 'slug': thread.slug})
  186. def set_description(self, description):
  187. self.description = description.strip()
  188. self.description_preparsed = ''
  189. if self.description:
  190. import markdown
  191. self.description_preparsed = markdown.markdown(description, safe_mode='escape', output_format=settings.OUTPUT_FORMAT)
  192. def copy_permissions(self, target):
  193. if target.pk != self.pk:
  194. from misago.models import Role
  195. for role in Role.objects.all():
  196. perms = role.permissions
  197. try:
  198. perms['forums'][self.pk] = perms['forums'][target.pk]
  199. role.permissions = perms
  200. role.save(force_update=True)
  201. except KeyError:
  202. pass
  203. def move_content(self, target):
  204. move_forum_content.send(sender=self, move_to=target)
  205. def sync_name(self):
  206. rename_forum.send(sender=self)
  207. def attr(self, att):
  208. if self.attrs:
  209. return att in self.attrs.split()
  210. return False
  211. def redirect_domain(self):
  212. hostname = urlparse.urlparse(self.redirect).hostname
  213. scheme = urlparse.urlparse(self.redirect).scheme
  214. if scheme:
  215. scheme = '%s://' % scheme
  216. return '%s%s' % (scheme, hostname)
  217. def new_last_thread(self, thread):
  218. self.last_thread = thread
  219. self.last_thread_name = thread.name
  220. self.last_thread_slug = thread.slug
  221. self.last_thread_date = thread.last
  222. self.last_poster = thread.last_poster
  223. self.last_poster_name = thread.last_poster_name
  224. self.last_poster_slug = thread.last_poster_slug
  225. self.last_poster_style = thread.last_poster_style
  226. def sync(self):
  227. self.threads = self.thread_set.filter(moderated=False).filter(deleted=False).count()
  228. self.posts = self.post_set.filter(moderated=False).count()
  229. self.last_poster = None
  230. self.last_poster_name = None
  231. self.last_poster_slug = None
  232. self.last_poster_style = None
  233. self.last_thread = None
  234. self.last_thread_date = None
  235. self.last_thread_name = None
  236. self.last_thread_slug = None
  237. try:
  238. last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[0:][0]
  239. self.last_poster_name = last_thread.last_poster_name
  240. self.last_poster_slug = last_thread.last_poster_slug
  241. self.last_poster_style = last_thread.last_poster_style
  242. if last_thread.last_poster:
  243. self.last_poster = last_thread.last_poster
  244. self.last_thread = last_thread
  245. self.last_thread_date = last_thread.last
  246. self.last_thread_name = last_thread.name
  247. self.last_thread_slug = last_thread.slug
  248. except (IndexError, AttributeError):
  249. pass
  250. def prune(self):
  251. pass
  252. """
  253. Signals
  254. """
  255. def rename_user_handler(sender, **kwargs):
  256. Forum.objects.filter(last_poster=sender).update(
  257. last_poster_name=sender.username,
  258. last_poster_slug=sender.username_slug,
  259. )
  260. rename_user.connect(rename_user_handler, dispatch_uid='rename_forums_last_poster')