forummodel.py 11 KB

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