thread.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. from django.core.urlresolvers import reverse
  2. from django.db import models, transaction
  3. from django.dispatch import receiver
  4. from misago.conf import settings
  5. from misago.core.shortcuts import paginate
  6. from misago.core.utils import slugify
  7. __all__ = [
  8. 'Thread',
  9. 'ThreadParticipant'
  10. ]
  11. class PrivateThreadMixin(object):
  12. pass
  13. class Thread(models.Model, PrivateThreadMixin):
  14. forum = models.ForeignKey('misago_forums.Forum')
  15. label = models.ForeignKey('misago_threads.Label',
  16. null=True, blank=True,
  17. on_delete=models.SET_NULL)
  18. title = models.CharField(max_length=255)
  19. slug = models.CharField(max_length=255)
  20. replies = models.PositiveIntegerField(default=0, db_index=True)
  21. has_reported_posts = models.BooleanField(default=False)
  22. has_moderated_posts = models.BooleanField(default=False)
  23. has_hidden_posts = models.BooleanField(default=False)
  24. has_events = models.BooleanField(default=False)
  25. started_on = models.DateTimeField(db_index=True)
  26. first_post = models.ForeignKey('misago_threads.Post', related_name='+',
  27. null=True, blank=True,
  28. on_delete=models.SET_NULL)
  29. starter = models.ForeignKey(settings.AUTH_USER_MODEL,
  30. null=True, blank=True,
  31. on_delete=models.SET_NULL)
  32. starter_name = models.CharField(max_length=255)
  33. starter_slug = models.CharField(max_length=255)
  34. last_post_on = models.DateTimeField(db_index=True)
  35. last_post = models.ForeignKey('misago_threads.Post', related_name='+',
  36. null=True, blank=True,
  37. on_delete=models.SET_NULL)
  38. last_poster = models.ForeignKey(settings.AUTH_USER_MODEL,
  39. related_name='last_poster_set',
  40. null=True, blank=True,
  41. on_delete=models.SET_NULL)
  42. last_poster_name = models.CharField(max_length=255, null=True, blank=True)
  43. last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
  44. is_pinned = models.BooleanField(default=False, db_index=True)
  45. is_poll = models.BooleanField(default=False)
  46. is_moderated = models.BooleanField(default=False, db_index=True)
  47. is_hidden = models.BooleanField(default=False)
  48. is_closed = models.BooleanField(default=False)
  49. participants = models.ManyToManyField(settings.AUTH_USER_MODEL,
  50. related_name='private_thread_set',
  51. through='ThreadParticipant',
  52. through_fields=('thread', 'user'))
  53. class Meta:
  54. index_together = [
  55. ['forum', 'id'],
  56. ['forum', 'last_post_on'],
  57. ['forum', 'replies'],
  58. ]
  59. def __unicode__(self):
  60. return self.title
  61. def lock(self):
  62. return Thread.objects.select_for_update().get(id=self.id)
  63. def delete(self, *args, **kwargs):
  64. from misago.threads.signals import delete_thread
  65. delete_thread.send(sender=self)
  66. super(Thread, self).delete(*args, **kwargs)
  67. def merge(self, other_thread):
  68. if self.pk == other_thread.pk:
  69. raise ValueError("thread can't be merged with itself")
  70. from misago.threads.signals import merge_thread
  71. merge_thread.send(sender=self, other_thread=other_thread)
  72. def move(self, new_forum):
  73. from misago.threads.signals import move_thread
  74. self.forum = new_forum
  75. move_thread.send(sender=self)
  76. def synchronize(self):
  77. self.replies = self.post_set.filter(is_moderated=False).count()
  78. if self.replies > 0:
  79. self.replies -= 1
  80. reported_post_qs = self.post_set.filter(is_reported=True)
  81. self.has_reported_posts = reported_post_qs.exists()
  82. moderated_post_qs = self.post_set.filter(is_moderated=True)
  83. self.has_moderated_posts = moderated_post_qs.exists()
  84. hidden_post_qs = self.post_set.filter(is_hidden=True)[:1]
  85. self.has_hidden_posts = hidden_post_qs.exists()
  86. self.has_events = self.event_set.exists()
  87. first_post = self.post_set.order_by('id')[:1][0]
  88. self.set_first_post(first_post)
  89. self.is_moderated = first_post.is_moderated
  90. self.is_hidden = first_post.is_hidden
  91. last_post_qs = self.post_set.filter(is_moderated=False).order_by('-id')
  92. last_post = last_post_qs[:1]
  93. if last_post:
  94. self.set_last_post(last_post[0])
  95. else:
  96. self.set_last_post(first_post)
  97. @property
  98. def thread_type(self):
  99. return self.forum.thread_type
  100. def get_absolute_url(self):
  101. return self.thread_type.get_thread_absolute_url(self)
  102. def get_post_url(self, post, page):
  103. try:
  104. post_id = post.id
  105. except AttributeError:
  106. post_id = post
  107. return self.thread_type.get_thread_post_url(self, post_id, page)
  108. def get_last_reply_url(self):
  109. return self.thread_type.get_thread_last_reply_url(self)
  110. def get_new_reply_url(self):
  111. return self.thread_type.get_thread_new_reply_url(self)
  112. def get_moderated_url(self):
  113. return self.thread_type.get_thread_moderated_url(self)
  114. def get_reported_url(self):
  115. return self.thread_type.get_thread_reported_url(self)
  116. def get_reply_api_url(self):
  117. return self.thread_type.get_reply_url(self)
  118. def set_title(self, title):
  119. self.title = title
  120. self.slug = slugify(title)
  121. def set_first_post(self, post):
  122. self.started_on = post.posted_on
  123. self.first_post = post
  124. self.starter = post.poster
  125. self.starter_name = post.poster_name
  126. if post.poster:
  127. self.starter_slug = post.poster.slug
  128. else:
  129. self.starter_slug = slugify(post.poster_name)
  130. self.is_moderated = post.is_moderated
  131. self.is_hidden = post.is_hidden
  132. def set_last_post(self, post):
  133. self.last_post_on = post.posted_on
  134. self.last_post = post
  135. self.last_poster = post.poster
  136. self.last_poster_name = post.poster_name
  137. if post.poster:
  138. self.last_poster_slug = post.poster.slug
  139. else:
  140. self.last_poster_slug = slugify(post.poster_name)
  141. class ThreadParticipantManager(models.Manager):
  142. def remove_participant(self, thread, user):
  143. ThreadParticipant.objects.filter(thread=thread, user=user).delete()
  144. @transaction.atomic
  145. def set_owner(self, thread, user):
  146. thread_owner = ThreadParticipant.objects.filter(
  147. thread=thread, is_owner=True)
  148. thread_owner.update(is_owner=False)
  149. self.remove_participant(thread, user)
  150. ThreadParticipant.objects.create(
  151. thread=thread,
  152. user=user,
  153. is_owner=True)
  154. @transaction.atomic
  155. def add_participant(self, thread, user):
  156. self.remove_participant(thread, user)
  157. ThreadParticipant.objects.create(
  158. thread=thread,
  159. user=user,)
  160. class ThreadParticipant(models.Model):
  161. thread = models.ForeignKey(Thread)
  162. user = models.ForeignKey(settings.AUTH_USER_MODEL)
  163. is_owner = models.BooleanField(default=False)
  164. objects = ThreadParticipantManager()