forummodel.py 14 KB

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