forummodel.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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. def forum_by_name(self, forum, acl):
  126. forums = self.readable_forums(acl, True)
  127. forum = forum.lower()
  128. for f in forums:
  129. f = self.forums_tree[f]
  130. if forum == unicode(f).lower():
  131. return f
  132. forum_len = len(forum)
  133. for f in forums:
  134. f = self.forums_tree[f]
  135. name = unicode(f).lower()
  136. if forum == unicode(f).lower()[0:forum_len]:
  137. return f
  138. return None
  139. class Forum(MPTTModel):
  140. parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
  141. type = models.CharField(max_length=12)
  142. special = models.CharField(max_length=255, null=True, blank=True)
  143. name = models.CharField(max_length=255)
  144. slug = models.SlugField(max_length=255)
  145. description = models.TextField(null=True, blank=True)
  146. description_preparsed = models.TextField(null=True, blank=True)
  147. threads = models.PositiveIntegerField(default=0)
  148. threads_delta = models.PositiveIntegerField(default=0)
  149. posts = models.PositiveIntegerField(default=0)
  150. posts_delta = models.IntegerField(default=0)
  151. redirects = models.PositiveIntegerField(default=0)
  152. redirects_delta = models.IntegerField(default=0)
  153. last_thread = models.ForeignKey('Thread', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  154. last_thread_name = models.CharField(max_length=255, null=True, blank=True)
  155. last_thread_slug = models.SlugField(max_length=255, null=True, blank=True)
  156. last_thread_date = models.DateTimeField(null=True, blank=True)
  157. last_poster = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  158. last_poster_name = models.CharField(max_length=255, null=True, blank=True)
  159. last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
  160. last_poster_style = models.CharField(max_length=255, null=True, blank=True)
  161. prune_start = models.PositiveIntegerField(default=0)
  162. prune_last = models.PositiveIntegerField(default=0)
  163. pruned_archive = models.ForeignKey('self', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  164. redirect = models.CharField(max_length=255, null=True, blank=True)
  165. attrs = models.CharField(max_length=255, null=True, blank=True)
  166. show_details = models.BooleanField(default=True)
  167. style = models.CharField(max_length=255, null=True, blank=True)
  168. closed = models.BooleanField(default=False)
  169. objects = ForumManager()
  170. class Meta:
  171. app_label = 'misago'
  172. def save(self, *args, **kwargs):
  173. super(Forum, self).save(*args, **kwargs)
  174. cache.delete('forums_tree')
  175. def delete(self, *args, **kwargs):
  176. delete_forum_content.send(sender=self)
  177. super(Forum, self).delete(*args, **kwargs)
  178. cache.delete('forums_tree')
  179. def __unicode__(self):
  180. if self.special == 'private_threads':
  181. return unicode(_('Private Threads'))
  182. if self.special == 'reports':
  183. return unicode(_('Reports'))
  184. if self.special == 'root':
  185. return unicode(_('Root Category'))
  186. return unicode(self.name)
  187. @property
  188. def url(self):
  189. if self.special == 'private_threads':
  190. reverse('private_threads')
  191. if self.special == 'reports':
  192. reverse('reports')
  193. if self.type == 'category':
  194. return reverse('category', kwargs={'forum': self.pk, 'slug': self.slug})
  195. if self.type == 'redirect':
  196. return reverse('redirect', kwargs={'forum': self.pk, 'slug': self.slug})
  197. return reverse('forum', kwargs={'forum': self.pk, 'slug': self.slug})
  198. def thread_link(self, extra):
  199. if self.special == 'private_threads':
  200. route_prefix = 'private_thread'
  201. if self.special == 'reports':
  202. route_prefix = 'report'
  203. else:
  204. route_prefix = 'thread'
  205. if extra:
  206. return '%s_%s' % (route_prefix, extra) if extra else route_prefix
  207. return route_prefix
  208. def thread_url(self, thread, route=None):
  209. route_prefix = 'thread'
  210. if self.special:
  211. route_prefix = self.special[0:-1]
  212. link = '%s_%s' % (route_prefix, route) if route else route_prefix
  213. return reverse(link, kwargs={'thread': thread.pk, 'slug': thread.slug})
  214. def set_description(self, description):
  215. self.description = description.strip()
  216. self.description_preparsed = ''
  217. if self.description:
  218. import markdown
  219. self.description_preparsed = markdown.markdown(description, safe_mode='escape', output_format=settings.OUTPUT_FORMAT)
  220. def copy_permissions(self, target):
  221. if target.pk != self.pk:
  222. from misago.models import Role
  223. for role in Role.objects.all():
  224. perms = role.permissions
  225. try:
  226. perms['forums'][self.pk] = perms['forums'][target.pk]
  227. role.permissions = perms
  228. role.save(force_update=True)
  229. except KeyError:
  230. pass
  231. def move_content(self, target):
  232. move_forum_content.send(sender=self, move_to=target)
  233. def sync_name(self):
  234. rename_forum.send(sender=self)
  235. def attr(self, att):
  236. if self.attrs:
  237. return att in self.attrs.split()
  238. return False
  239. def redirect_domain(self):
  240. hostname = urlparse.urlparse(self.redirect).hostname
  241. scheme = urlparse.urlparse(self.redirect).scheme
  242. if scheme:
  243. scheme = '%s://' % scheme
  244. return '%s%s' % (scheme, hostname)
  245. def new_last_thread(self, thread):
  246. self.last_thread = thread
  247. self.last_thread_name = thread.name
  248. self.last_thread_slug = thread.slug
  249. self.last_thread_date = thread.last
  250. self.last_poster = thread.last_poster
  251. self.last_poster_name = thread.last_poster_name
  252. self.last_poster_slug = thread.last_poster_slug
  253. self.last_poster_style = thread.last_poster_style
  254. def sync_last(self):
  255. self.last_poster = None
  256. self.last_poster_name = None
  257. self.last_poster_slug = None
  258. self.last_poster_style = None
  259. self.last_thread = None
  260. self.last_thread_date = None
  261. self.last_thread_name = None
  262. self.last_thread_slug = None
  263. try:
  264. last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[:1][0]
  265. self.last_poster_name = last_thread.last_poster_name
  266. self.last_poster_slug = last_thread.last_poster_slug
  267. self.last_poster_style = last_thread.last_poster_style
  268. if last_thread.last_poster:
  269. self.last_poster = last_thread.last_poster
  270. self.last_thread = last_thread
  271. self.last_thread_date = last_thread.last
  272. self.last_thread_name = last_thread.name
  273. self.last_thread_slug = last_thread.slug
  274. except (IndexError, AttributeError):
  275. pass
  276. def sync(self):
  277. self.threads = self.thread_set.filter(moderated=False).filter(deleted=False).count()
  278. self.posts = self.post_set.filter(moderated=False).count()
  279. self.sync_last()
  280. def prune(self):
  281. pass
  282. """
  283. Signals
  284. """
  285. def rename_user_handler(sender, **kwargs):
  286. Forum.objects.filter(last_poster=sender).update(
  287. last_poster_name=sender.username,
  288. last_poster_slug=sender.username_slug,
  289. )
  290. rename_user.connect(rename_user_handler, dispatch_uid='rename_forums_last_poster')