models.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. from datetime import timedelta
  2. from django.conf import settings
  3. from django.db import models
  4. from django.db.models import F
  5. from django.utils import timezone
  6. from django.utils.translation import ugettext_lazy as _
  7. from misago.forums.signals import move_forum_content
  8. from misago.threads.signals import move_thread, merge_thread, move_post, merge_post
  9. from misago.users.signals import delete_user_content, rename_user
  10. from misago.utils import slugify, ugettext_lazy
  11. from misago.readstracker.models import ForumRecord, ThreadRecord
  12. from misago.watcher.models import ThreadWatch
  13. class ThreadManager(models.Manager):
  14. def filter_stats(self, start, end):
  15. return self.filter(start__gte=start).filter(start__lte=end)
  16. def with_reads(self, queryset, user):
  17. threads = []
  18. threads_dict = {}
  19. forum_reads = {}
  20. cutoff = timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH)
  21. if user.is_authenticated() and user.join_date > cutoff:
  22. cutoff = user.join_date
  23. for row in ForumRecord.objects.filter(user=user).values('forum_id', 'cleared'):
  24. forum_reads[row['forum_id']] = row['cleared']
  25. for thread in queryset:
  26. thread.is_read = True
  27. if user.is_authenticated() and thread.last > cutoff:
  28. try:
  29. thread.is_read = thread.last <= forum_reads[thread.forum_id]
  30. except KeyError:
  31. pass
  32. threads.append(thread)
  33. threads_dict[thread.pk] = thread
  34. if user.is_authenticated():
  35. for read in ThreadRecord.objects.filter(user=user).filter(thread__in=threads_dict.keys()):
  36. try:
  37. threads_dict[read.thread_id].is_read = (threads_dict[read.thread_id].last <= cutoff or
  38. threads_dict[read.thread_id].last <= read.updated or
  39. threads_dict[read.thread_id].last <= forum_reads[read.forum_id])
  40. except KeyError:
  41. pass
  42. return threads
  43. class Thread(models.Model):
  44. forum = models.ForeignKey('forums.Forum')
  45. weight = models.PositiveIntegerField(default=0)
  46. name = models.CharField(max_length=255)
  47. slug = models.SlugField(max_length=255)
  48. replies = models.PositiveIntegerField(default=0)
  49. replies_reported = models.PositiveIntegerField(default=0)
  50. replies_moderated = models.PositiveIntegerField(default=0)
  51. replies_deleted = models.PositiveIntegerField(default=0)
  52. merges = models.PositiveIntegerField(default=0)
  53. score = models.PositiveIntegerField(default=30)
  54. upvotes = models.PositiveIntegerField(default=0)
  55. downvotes = models.PositiveIntegerField(default=0)
  56. start = models.DateTimeField()
  57. start_post = models.ForeignKey('Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  58. start_poster = models.ForeignKey('users.User', null=True, blank=True, on_delete=models.SET_NULL)
  59. start_poster_name = models.CharField(max_length=255)
  60. start_poster_slug = models.SlugField(max_length=255)
  61. start_poster_style = models.CharField(max_length=255, null=True, blank=True)
  62. last = models.DateTimeField()
  63. last_post = models.ForeignKey('Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  64. last_poster = models.ForeignKey('users.User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  65. last_poster_name = models.CharField(max_length=255, null=True, blank=True)
  66. last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
  67. last_poster_style = models.CharField(max_length=255, null=True, blank=True)
  68. moderated = models.BooleanField(default=False)
  69. deleted = models.BooleanField(default=False)
  70. closed = models.BooleanField(default=False)
  71. objects = ThreadManager()
  72. statistics_name = _('New Threads')
  73. def get_date(self):
  74. return self.start
  75. def move_to(self, move_to):
  76. move_thread.send(sender=self, move_to=move_to)
  77. self.forum = move_to
  78. def merge_with(self, thread, merge):
  79. merge_thread.send(sender=self, new_thread=thread, merge=merge)
  80. def sync(self):
  81. # Counters
  82. self.replies = self.post_set.filter(moderated=False).count() - 1
  83. if self.replies < 0:
  84. self.replies = 0
  85. self.replies_reported = self.post_set.filter(reported=True).count()
  86. self.replies_moderated = self.post_set.filter(moderated=True).count()
  87. self.replies_deleted = self.post_set.filter(deleted=True).count()
  88. # First post
  89. start_post = self.post_set.order_by('merge', 'id')[0:][0]
  90. self.start = start_post.date
  91. self.start_post = start_post
  92. self.start_poster = start_post.user
  93. self.start_poster_name = start_post.user_name
  94. self.start_poster_slug = slugify(start_post.user_name)
  95. self.start_poster_style = start_post.user.rank.style if start_post.user else ''
  96. self.upvotes = start_post.upvotes
  97. self.downvotes = start_post.downvotes
  98. # Last visible post
  99. if self.replies > 0:
  100. last_post = self.post_set.order_by('-merge', '-id').filter(moderated=False)[0:][0]
  101. else:
  102. last_post = start_post
  103. self.last = last_post.date
  104. self.last_post = last_post
  105. self.last_poster = last_post.user
  106. self.last_poster_name = last_post.user_name
  107. self.last_poster_slug = slugify(last_post.user_name)
  108. self.last_poster_style = last_post.user.rank.style if last_post.user else ''
  109. # Flags
  110. self.moderated = start_post.moderated
  111. self.deleted = start_post.deleted
  112. self.merges = last_post.merge
  113. def email_watchers(self, request, post):
  114. from misago.acl.builder import get_acl
  115. from misago.acl.utils import ACLError403, ACLError404
  116. for watch in ThreadWatch.objects.filter(thread=self).filter(email=True).filter(last_read__gte=self.previous_last):
  117. user = watch.user
  118. if user.pk != request.user.pk:
  119. try:
  120. acl = get_acl(request, user)
  121. acl.forums.allow_forum_view(self.forum)
  122. acl.threads.allow_thread_view(user, self)
  123. acl.threads.allow_post_view(user, self, post)
  124. if not user.is_ignoring(request.user):
  125. user.email_user(
  126. request,
  127. 'post_notification',
  128. _('New reply in thread "%(thread)s"') % {'thread': self.name},
  129. {'author': request.user, 'post': post, 'thread': self}
  130. )
  131. except (ACLError403, ACLError404):
  132. pass
  133. class PostManager(models.Manager):
  134. def filter_stats(self, start, end):
  135. return self.filter(date__gte=start).filter(date__lte=end)
  136. class Post(models.Model):
  137. forum = models.ForeignKey('forums.Forum')
  138. thread = models.ForeignKey(Thread)
  139. merge = models.PositiveIntegerField(default=0)
  140. user = models.ForeignKey('users.User', null=True, blank=True, on_delete=models.SET_NULL)
  141. user_name = models.CharField(max_length=255)
  142. ip = models.GenericIPAddressField()
  143. agent = models.CharField(max_length=255)
  144. post = models.TextField()
  145. post_preparsed = models.TextField()
  146. upvotes = models.PositiveIntegerField(default=0)
  147. downvotes = models.PositiveIntegerField(default=0)
  148. mentions = models.ManyToManyField('users.User', related_name="mention_set")
  149. checkpoints = models.BooleanField(default=False)
  150. date = models.DateTimeField()
  151. edits = models.PositiveIntegerField(default=0)
  152. edit_date = models.DateTimeField(null=True, blank=True)
  153. edit_reason = models.CharField(max_length=255, null=True, blank=True)
  154. edit_user = models.ForeignKey('users.User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
  155. edit_user_name = models.CharField(max_length=255, null=True, blank=True)
  156. edit_user_slug = models.SlugField(max_length=255, null=True, blank=True)
  157. reported = models.BooleanField(default=False)
  158. moderated = models.BooleanField(default=False)
  159. deleted = models.BooleanField(default=False)
  160. protected = models.BooleanField(default=False)
  161. objects = PostManager()
  162. statistics_name = _('New Posts')
  163. def get_date(self):
  164. return self.date
  165. def move_to(self, thread):
  166. move_post.send(sender=self, move_to=thread)
  167. self.thread = thread
  168. self.forum = thread.forum
  169. def merge_with(self, post):
  170. post.post = '%s\n- - -\n%s' % (post.post, self.post)
  171. merge_post.send(sender=self, new_post=post)
  172. def set_checkpoint(self, request, action):
  173. if request.user.is_authenticated():
  174. self.checkpoints = True
  175. self.checkpoint_set.create(
  176. forum=self.forum,
  177. thread=self.thread,
  178. post=self,
  179. action=action,
  180. user=request.user,
  181. user_name=request.user.username,
  182. user_slug=request.user.username_slug,
  183. date=timezone.now(),
  184. ip=request.session.get_ip(request),
  185. agent=request.META.get('HTTP_USER_AGENT'),
  186. )
  187. def notify_mentioned(self, request, users):
  188. from misago.acl.builder import get_acl
  189. from misago.acl.utils import ACLError403, ACLError404
  190. mentioned = self.mentions.all()
  191. for slug, user in users.items():
  192. if user.pk != request.user.pk and user not in mentioned:
  193. self.mentions.add(user)
  194. try:
  195. acl = get_acl(request, user)
  196. acl.forums.allow_forum_view(self.forum)
  197. acl.threads.allow_thread_view(user, self.thread)
  198. acl.threads.allow_post_view(user, self.thread, self)
  199. if not user.is_ignoring(request.user):
  200. alert = user.alert(ugettext_lazy("%(username)s has mentioned you in his reply in thread %(thread)s").message)
  201. alert.profile('username', request.user)
  202. alert.post('thread', self.thread, self)
  203. alert.save_all()
  204. except (ACLError403, ACLError404):
  205. pass
  206. class Karma(models.Model):
  207. forum = models.ForeignKey('forums.Forum')
  208. thread = models.ForeignKey(Thread)
  209. post = models.ForeignKey(Post)
  210. user = models.ForeignKey('users.User', null=True, blank=True, on_delete=models.SET_NULL)
  211. user_name = models.CharField(max_length=255)
  212. user_slug = models.CharField(max_length=255)
  213. date = models.DateTimeField()
  214. ip = models.GenericIPAddressField()
  215. agent = models.CharField(max_length=255)
  216. score = models.IntegerField(default=0)
  217. class Change(models.Model):
  218. forum = models.ForeignKey('forums.Forum')
  219. thread = models.ForeignKey(Thread)
  220. post = models.ForeignKey(Post)
  221. user = models.ForeignKey('users.User', null=True, blank=True, on_delete=models.SET_NULL)
  222. user_name = models.CharField(max_length=255)
  223. user_slug = models.CharField(max_length=255)
  224. date = models.DateTimeField()
  225. ip = models.GenericIPAddressField()
  226. agent = models.CharField(max_length=255)
  227. reason = models.CharField(max_length=255, null=True, blank=True)
  228. thread_name_new = models.CharField(max_length=255, null=True, blank=True)
  229. thread_name_old = models.CharField(max_length=255, null=True, blank=True)
  230. post_content = models.TextField()
  231. size = models.IntegerField(default=0)
  232. change = models.IntegerField(default=0)
  233. class Checkpoint(models.Model):
  234. forum = models.ForeignKey('forums.Forum')
  235. thread = models.ForeignKey(Thread)
  236. post = models.ForeignKey(Post)
  237. action = models.CharField(max_length=255)
  238. user = models.ForeignKey('users.User', null=True, blank=True, on_delete=models.SET_NULL)
  239. user_name = models.CharField(max_length=255)
  240. user_slug = models.CharField(max_length=255)
  241. date = models.DateTimeField()
  242. ip = models.GenericIPAddressField()
  243. agent = models.CharField(max_length=255)
  244. """
  245. Signals
  246. """
  247. def rename_user_handler(sender, **kwargs):
  248. Thread.objects.filter(start_poster=sender).update(
  249. start_poster_name=sender.username,
  250. start_poster_slug=sender.username_slug,
  251. )
  252. Thread.objects.filter(last_poster=sender).update(
  253. last_poster_name=sender.username,
  254. last_poster_slug=sender.username_slug,
  255. )
  256. Post.objects.filter(user=sender).update(
  257. user_name=sender.username,
  258. )
  259. Post.objects.filter(edit_user=sender).update(
  260. edit_user_name=sender.username,
  261. edit_user_slug=sender.username_slug,
  262. )
  263. Karma.objects.filter(user=sender).update(
  264. user_name=sender.username,
  265. user_slug=sender.username_slug,
  266. )
  267. Change.objects.filter(user=sender).update(
  268. user_name=sender.username,
  269. user_slug=sender.username_slug,
  270. )
  271. Checkpoint.objects.filter(user=sender).update(
  272. user_name=sender.username,
  273. user_slug=sender.username_slug,
  274. )
  275. rename_user.connect(rename_user_handler, dispatch_uid="rename_user_threads")
  276. def delete_user_content_handler(sender, **kwargs):
  277. for thread in Thread.objects.filter(start_poster=sender):
  278. thread.delete()
  279. threads = []
  280. prev_posts = []
  281. for post in sender.post_set.filter(checkpoints=True):
  282. threads.append(post.thread_id)
  283. prev_post = Post.objects.filter(thread=post.thread_id).exclude(merge__gt=post.merge).exclude(user=sender).order_by('merge', '-id')[:1][0]
  284. post.checkpoint_set.update(post=prev_post)
  285. if not prev_post.pk in prev_posts:
  286. prev_posts.append(prev_post.pk)
  287. sender.post_set.all().delete()
  288. Post.objects.filter(id__in=prev_posts).update(checkpoints=True)
  289. for post in sender.post_set.distinct().values('thread_id').iterator():
  290. if not post['thread_id'] in threads:
  291. threads.append(post['thread_id'])
  292. for post in Post.objects.filter(user=sender):
  293. post.delete()
  294. for thread in Thread.objects.filter(id__in=threads):
  295. thread.sync()
  296. thread.save(force_update=True)
  297. delete_user_content.connect(delete_user_content_handler, dispatch_uid="delete_user_threads_posts")
  298. def move_forum_content_handler(sender, **kwargs):
  299. Thread.objects.filter(forum=sender).update(forum=kwargs['move_to'])
  300. Post.objects.filter(forum=sender).update(forum=kwargs['move_to'])
  301. Karma.objects.filter(forum=sender).update(forum=kwargs['move_to'])
  302. Change.objects.filter(forum=sender).update(forum=kwargs['move_to'])
  303. Checkpoint.objects.filter(forum=sender).update(forum=kwargs['move_to'])
  304. move_forum_content.connect(move_forum_content_handler, dispatch_uid="move_forum_threads_posts")
  305. def move_thread_handler(sender, **kwargs):
  306. Post.objects.filter(thread=sender).update(forum=kwargs['move_to'])
  307. Karma.objects.filter(thread=sender).update(forum=kwargs['move_to'])
  308. Change.objects.filter(thread=sender).update(forum=kwargs['move_to'])
  309. Checkpoint.objects.filter(thread=sender).update(forum=kwargs['move_to'])
  310. move_thread.connect(move_thread_handler, dispatch_uid="move_thread")
  311. def merge_thread_handler(sender, **kwargs):
  312. Post.objects.filter(thread=sender).update(thread=kwargs['new_thread'], merge=F('merge') + kwargs['merge'])
  313. Karma.objects.filter(thread=sender).update(thread=kwargs['new_thread'])
  314. Change.objects.filter(thread=sender).update(thread=kwargs['new_thread'])
  315. Checkpoint.objects.filter(thread=sender).delete()
  316. merge_thread.connect(merge_thread_handler, dispatch_uid="merge_threads")
  317. def move_posts_handler(sender, **kwargs):
  318. Change.objects.filter(post=sender).update(forum=kwargs['move_to'].forum, thread=kwargs['move_to'])
  319. Karma.objects.filter(post=sender).update(forum=kwargs['move_to'].forum, thread=kwargs['move_to'])
  320. if sender.checkpoints:
  321. prev_post = Post.objects.filter(thread=sender.thread_id).filter(merge__lte=sender.merge).exclude(id=sender.pk).order_by('merge', '-id')[:1][0]
  322. Checkpoint.objects.filter(post=sender).update(post=prev_post)
  323. prev_post.checkpoints = True
  324. prev_post.save(force_update=True)
  325. sender.checkpoints = False
  326. move_post.connect(move_posts_handler, dispatch_uid="move_posts")
  327. def merge_posts_handler(sender, **kwargs):
  328. Change.objects.filter(post=sender).update(post=kwargs['new_post'])
  329. Checkpoint.objects.filter(post=sender).update(post=kwargs['new_post'])
  330. Karma.objects.filter(post=sender).update(post=kwargs['new_post'])
  331. if sender.checkpoints:
  332. kwargs['new_post'].checkpoints = True
  333. kwargs['new_post'].upvotes += self.upvotes
  334. kwargs['new_post'].downvotes += self.downvotes
  335. kwargs['new_post'].score += self.score
  336. merge_post.connect(merge_posts_handler, dispatch_uid="merge_posts")