forummodel.py 14 KB

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