threadmodel.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. from datetime import timedelta
  2. from django.conf import settings
  3. from django.db import models
  4. from django.db.models.signals import pre_save, pre_delete
  5. from django.utils import timezone
  6. from django.utils.translation import ugettext_lazy as _
  7. from misago.signals import (delete_user_content, merge_thread, move_forum_content,
  8. move_thread, rename_user, sync_user_profile, remove_thread_prefix)
  9. from misago.utils.strings import slugify
  10. class ThreadManager(models.Manager):
  11. def filter_stats(self, start, end):
  12. return self.filter(start__gte=start).filter(start__lte=end)
  13. def with_reads(self, queryset, user):
  14. from misago.models import ForumRead, ThreadRead
  15. threads = []
  16. threads_dict = {}
  17. forum_reads = {}
  18. cutoff = timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH)
  19. if user.is_authenticated() and user.join_date > cutoff:
  20. cutoff = user.join_date
  21. for row in ForumRead.objects.filter(user=user).values('forum_id', 'cleared').iterator():
  22. forum_reads[row['forum_id']] = row['cleared']
  23. for thread in queryset:
  24. thread.is_read = True
  25. if user.is_authenticated() and thread.last > cutoff:
  26. try:
  27. thread.is_read = thread.last <= forum_reads[thread.forum_id]
  28. except KeyError:
  29. pass
  30. threads.append(thread)
  31. threads_dict[thread.pk] = thread
  32. if user.is_authenticated():
  33. for read in ThreadRead.objects.filter(user=user).filter(thread__in=threads_dict.keys()).iterator():
  34. try:
  35. threads_dict[read.thread_id].is_read = (threads_dict[read.thread_id].last <= cutoff or
  36. threads_dict[read.thread_id].last <= read.updated or
  37. threads_dict[read.thread_id].last <= forum_reads[read.forum_id])
  38. except KeyError:
  39. pass
  40. return threads
  41. class Thread(models.Model):
  42. forum = models.ForeignKey('Forum')
  43. weight = models.PositiveIntegerField(default=0)
  44. prefix = models.ForeignKey('ThreadPrefix', null=True, blank=True, on_delete=models.SET_NULL)
  45. name = models.CharField(max_length=255)
  46. slug = models.SlugField(max_length=255)
  47. replies = models.PositiveIntegerField(default=0)
  48. replies_reported = models.PositiveIntegerField(default=0)
  49. replies_moderated = models.PositiveIntegerField(default=0)
  50. replies_deleted = models.PositiveIntegerField(default=0)
  51. score = models.PositiveIntegerField(default=30)
  52. upvotes = models.PositiveIntegerField(default=0)
  53. downvotes = models.PositiveIntegerField(default=0)
  54. start = models.DateTimeField()
  55. start_post = models.ForeignKey('Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  56. start_poster = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
  57. start_poster_name = models.CharField(max_length=255)
  58. start_poster_slug = models.SlugField(max_length=255)
  59. start_poster_style = models.CharField(max_length=255, null=True, blank=True)
  60. last = models.DateTimeField()
  61. last_post = models.ForeignKey('Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  62. last_poster = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  63. last_poster_name = models.CharField(max_length=255, null=True, blank=True)
  64. last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
  65. last_poster_style = models.CharField(max_length=255, null=True, blank=True)
  66. participants = models.ManyToManyField('User', related_name='private_thread_set')
  67. report_for = models.ForeignKey('Post', related_name='report_set', null=True, blank=True, on_delete=models.SET_NULL)
  68. has_poll = models.BooleanField(default=False)
  69. moderated = models.BooleanField(default=False)
  70. deleted = models.BooleanField(default=False)
  71. closed = models.BooleanField(default=False)
  72. objects = ThreadManager()
  73. statistics_name = _('New Threads')
  74. class Meta:
  75. app_label = 'misago'
  76. @property
  77. def timeline_date(self):
  78. return self.start
  79. @property
  80. def poll(self):
  81. if self.has_poll:
  82. return self.poll_of
  83. else:
  84. return None
  85. def delete(self, *args, **kwargs):
  86. """
  87. FUGLY HAX for weird stuff that happens with
  88. relations on model deletion in MySQL
  89. """
  90. if self.replies_reported:
  91. clear_reports = [post.pk for post in self.post_set.filter(reported=True).iterator()]
  92. if clear_reports:
  93. Thread.objects.filter(report_for__in=clear_reports).update(report_for=None)
  94. return super(Thread, self).delete(*args, **kwargs)
  95. def get_date(self):
  96. return self.start
  97. def add_checkpoints_to_posts(self, show_all, posts, start=None, stop=None):
  98. qs = self.checkpoint_set.all()
  99. if start:
  100. qs = qs.filter(date__gte=start)
  101. if stop:
  102. qs = qs.filter(date__lte=stop)
  103. if not show_all:
  104. qs = qs.filter(deleted=False)
  105. checkpoints = [i for i in qs]
  106. i_max = len(posts) - 1
  107. for i, post in enumerate(posts):
  108. post.checkpoints_visible = []
  109. for c in checkpoints:
  110. if ((i == 0 and c.date <= post.date)
  111. or (c.date >= post.date and (i == i_max or c.date < posts[i+1].date))):
  112. post.checkpoints_visible.append(c)
  113. def set_checkpoint(self, request, action, user=None, forum=None, extra=None):
  114. if request.user.is_authenticated():
  115. self.checkpoint_set.create(
  116. forum=self.forum,
  117. thread=self,
  118. action=action,
  119. extra=extra,
  120. user=request.user,
  121. user_name=request.user.username,
  122. user_slug=request.user.username_slug,
  123. date=timezone.now(),
  124. ip=request.session.get_ip(request),
  125. agent=request.META.get('HTTP_USER_AGENT'),
  126. target_user=user,
  127. target_user_name=(user.username if user else None),
  128. target_user_slug=(user.username_slug if user else None),
  129. old_forum=forum,
  130. old_forum_name=(forum.name if forum else None),
  131. old_forum_slug=(forum.slug if forum else None),
  132. )
  133. def new_start_post(self, post):
  134. self.start = post.date
  135. self.start_post = post
  136. self.start_poster = post.user
  137. self.start_poster_name = post.user.username
  138. self.start_poster_slug = post.user.username_slug
  139. if post.user.rank_id and post.user.rank.style:
  140. self.start_poster_style = post.user.rank.style
  141. def new_last_post(self, post):
  142. self.last = post.date
  143. self.last_post = post
  144. self.last_poster = post.user
  145. self.last_poster_name = post.user.username
  146. self.last_poster_slug = post.user.username_slug
  147. if post.user.rank_id and post.user.rank.style:
  148. self.last_poster_style = post.user.rank.style
  149. def move_to(self, move_to):
  150. move_thread.send(sender=self, move_to=move_to)
  151. self.forum = move_to
  152. def merge_with(self, thread):
  153. merge_thread.send(sender=self, new_thread=thread)
  154. def update_current_dates(self):
  155. self.post_set.update(current_date=timezone.now())
  156. def sync(self):
  157. # Counters
  158. self.replies = self.post_set.filter(moderated=False).count() - 1
  159. if self.replies < 0:
  160. self.replies = 0
  161. self.replies_reported = self.post_set.filter(reported=True).count()
  162. self.replies_moderated = self.post_set.filter(moderated=True).count()
  163. self.replies_deleted = self.post_set.filter(deleted=True).count()
  164. # First post
  165. start_post = self.post_set.order_by('id')[0:][0]
  166. self.start = start_post.date
  167. self.start_post = start_post
  168. self.start_poster = start_post.user
  169. self.start_poster_name = start_post.user_name
  170. self.start_poster_slug = slugify(start_post.user_name)
  171. self.start_poster_style = start_post.user.rank.style if start_post.user and start_post.user.rank else ''
  172. self.upvotes = start_post.upvotes
  173. self.downvotes = start_post.downvotes
  174. # Last visible post
  175. if self.replies > 0:
  176. last_post = self.post_set.order_by('-id').filter(moderated=False)[0:][0]
  177. else:
  178. last_post = start_post
  179. self.last = last_post.date
  180. self.last_post = last_post
  181. self.last_poster = last_post.user
  182. self.last_poster_name = last_post.user_name
  183. self.last_poster_slug = slugify(last_post.user_name)
  184. self.last_poster_style = last_post.user.rank.style if last_post.user and last_post.user.rank else ''
  185. # Flags
  186. self.moderated = start_post.moderated
  187. self.deleted = start_post.deleted
  188. def email_watchers(self, request, thread_type, post):
  189. from misago.acl.exceptions import ACLError403, ACLError404
  190. from misago.models import ThreadRead, WatchedThread
  191. notified = []
  192. for watch in WatchedThread.objects.filter(thread=self).filter(last_read__gte=self.previous_last.date):
  193. user = watch.user
  194. if user.pk != request.user.pk:
  195. try:
  196. user_acl = user.acl()
  197. user_acl.forums.allow_forum_view(self.forum)
  198. user_acl.threads.allow_thread_view(user, self)
  199. user_acl.threads.allow_post_view(user, self, post)
  200. if not user.is_ignoring(request.user):
  201. if watch.email:
  202. user.email_user(
  203. request,
  204. '%s_reply_notification' % thread_type,
  205. _('New reply in thread "%(thread)s"') % {'thread': self.name},
  206. {'author': request.user, 'post': post, 'thread': self}
  207. )
  208. notified.append(user)
  209. except (ACLError403, ACLError404):
  210. pass
  211. return notified
  212. def rename_user_handler(sender, **kwargs):
  213. Thread.objects.filter(start_poster=sender).update(
  214. start_poster_name=sender.username,
  215. start_poster_slug=sender.username_slug,
  216. )
  217. Thread.objects.filter(last_poster=sender).update(
  218. last_poster_name=sender.username,
  219. last_poster_slug=sender.username_slug,
  220. )
  221. rename_user.connect(rename_user_handler, dispatch_uid="rename_user_threads")
  222. def report_update_handler(sender, **kwargs):
  223. if sender == Thread:
  224. thread = kwargs.get('instance')
  225. if thread.weight < 2 and thread.report_for_id:
  226. reported_post = thread.report_for
  227. if reported_post.reported:
  228. reported_post.reported = False
  229. reported_post.reports = None
  230. reported_post.save(force_update=True)
  231. reported_post.thread.replies_reported -= 1
  232. reported_post.thread.save(force_update=True)
  233. pre_save.connect(report_update_handler, dispatch_uid="sync_post_reports_on_update")
  234. def report_delete_handler(sender, **kwargs):
  235. if sender == Thread:
  236. thread = kwargs.get('instance')
  237. if thread.report_for_id:
  238. reported_post = thread.report_for
  239. if reported_post.reported:
  240. reported_post.reported = False
  241. reported_post.reports = None
  242. reported_post.save(force_update=True)
  243. reported_post.thread.replies_reported -= 1
  244. reported_post.thread.save(force_update=True)
  245. pre_delete.connect(report_delete_handler, dispatch_uid="sync_post_reports_on_delete")
  246. def delete_user_content_handler(sender, **kwargs):
  247. for thread in Thread.objects.filter(start_poster=sender):
  248. thread.delete()
  249. delete_user_content.connect(delete_user_content_handler, dispatch_uid="delete_user_threads")
  250. def move_forum_content_handler(sender, **kwargs):
  251. Thread.objects.filter(forum=sender).update(forum=kwargs['move_to'])
  252. move_forum_content.connect(move_forum_content_handler, dispatch_uid="move_forum_threads")
  253. def delete_user_handler(sender, instance, using, **kwargs):
  254. from misago.models import User
  255. if sender == User:
  256. for thread in instance.private_thread_set.all():
  257. thread.participants.remove(instance)
  258. if not thread.participants.count():
  259. thread.delete()
  260. pre_delete.connect(delete_user_handler, dispatch_uid="delete_user_participations")
  261. def sync_user_handler(sender, **kwargs):
  262. sender.threads = sender.thread_set.count()
  263. sync_user_profile.connect(sync_user_handler, dispatch_uid="sync_user_threads")