thread.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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. 'PARTICIPANT_REMOVED',
  9. 'PARTICIPANT_ACTIVE',
  10. 'PARTICIPANT_OWNER',
  11. 'Thread',
  12. 'ThreadParticipant'
  13. ]
  14. PARTICIPANT_REMOVED = 0
  15. PARTICIPANT_ACTIVE = 1
  16. PARTICIPANT_OWNER = 2
  17. class PrivateThreadMixin(object):
  18. pass
  19. class Thread(models.Model, PrivateThreadMixin):
  20. forum = models.ForeignKey('misago_forums.Forum')
  21. label = models.ForeignKey('misago_threads.Label',
  22. null=True, blank=True,
  23. on_delete=models.SET_NULL)
  24. title = models.CharField(max_length=255)
  25. slug = models.CharField(max_length=255)
  26. replies = models.PositiveIntegerField(default=0, db_index=True)
  27. has_reported_posts = models.BooleanField(default=False)
  28. has_moderated_posts = models.BooleanField(default=False)
  29. has_hidden_posts = models.BooleanField(default=False)
  30. has_events = models.BooleanField(default=False)
  31. started_on = models.DateTimeField(db_index=True)
  32. first_post = models.ForeignKey('misago_threads.Post', related_name='+',
  33. null=True, blank=True,
  34. on_delete=models.SET_NULL)
  35. starter = models.ForeignKey(settings.AUTH_USER_MODEL,
  36. null=True, blank=True,
  37. on_delete=models.SET_NULL)
  38. starter_name = models.CharField(max_length=255)
  39. starter_slug = models.CharField(max_length=255)
  40. last_post_on = models.DateTimeField(db_index=True)
  41. last_post = models.ForeignKey('misago_threads.Post', related_name='+',
  42. null=True, blank=True,
  43. on_delete=models.SET_NULL)
  44. last_poster = models.ForeignKey(settings.AUTH_USER_MODEL,
  45. related_name='last_poster_set',
  46. null=True, blank=True,
  47. on_delete=models.SET_NULL)
  48. last_poster_name = models.CharField(max_length=255, null=True, blank=True)
  49. last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
  50. is_pinned = models.BooleanField(default=False, db_index=True)
  51. is_poll = models.BooleanField(default=False)
  52. is_moderated = models.BooleanField(default=False, db_index=True)
  53. is_hidden = models.BooleanField(default=False)
  54. is_closed = models.BooleanField(default=False)
  55. participants = models.ManyToManyField(settings.AUTH_USER_MODEL,
  56. related_name='private_thread_set',
  57. through='ThreadParticipant',
  58. through_fields=('thread', 'user'))
  59. class Meta:
  60. index_together = [
  61. ['forum', 'id'],
  62. ['forum', 'last_post_on'],
  63. ['forum', 'replies'],
  64. ]
  65. def __unicode__(self):
  66. return self.title
  67. def lock(self):
  68. return Thread.objects.select_for_update().get(id=self.id)
  69. def delete(self, *args, **kwargs):
  70. from misago.threads.signals import delete_thread
  71. delete_thread.send(sender=self)
  72. super(Thread, self).delete(*args, **kwargs)
  73. def merge(self, other_thread):
  74. if self.pk == other_thread.pk:
  75. raise ValueError("thread can't be merged with itself")
  76. from misago.threads.signals import merge_thread
  77. merge_thread.send(sender=self, other_thread=other_thread)
  78. def move(self, new_forum):
  79. from misago.threads.signals import move_thread
  80. self.forum = new_forum
  81. move_thread.send(sender=self)
  82. def synchronize(self):
  83. self.replies = self.post_set.filter(is_moderated=False).count()
  84. if self.replies > 0:
  85. self.replies -= 1
  86. reported_post_qs = self.post_set.filter(is_reported=True)
  87. self.has_reported_posts = reported_post_qs.exists()
  88. moderated_post_qs = self.post_set.filter(is_moderated=True)
  89. self.has_moderated_posts = moderated_post_qs.exists()
  90. hidden_post_qs = self.post_set.filter(is_hidden=True)[:1]
  91. self.has_hidden_posts = hidden_post_qs.exists()
  92. self.has_events = self.event_set.exists()
  93. first_post = self.post_set.order_by('id')[:1][0]
  94. self.set_first_post(first_post)
  95. self.is_moderated = first_post.is_moderated
  96. self.is_hidden = first_post.is_hidden
  97. last_post_qs = self.post_set.filter(is_moderated=False).order_by('-id')
  98. last_post = last_post_qs[:1]
  99. if last_post:
  100. self.set_last_post(last_post[0])
  101. else:
  102. self.set_last_post(first_post)
  103. @property
  104. def link_prefix(self):
  105. if self.forum.special_role == 'private_threads':
  106. return 'private_thread'
  107. else:
  108. return 'thread'
  109. def get_url_name(self, suffix=None):
  110. link = 'misago:%s' % self.link_prefix
  111. if suffix:
  112. link = '%s_%s' % (link, suffix)
  113. return link
  114. def get_url(self, suffix=None):
  115. return reverse(self.get_url_name(suffix), kwargs={
  116. 'thread_slug': self.slug,
  117. 'thread_id': self.id
  118. })
  119. def get_absolute_url(self):
  120. return self.get_url()
  121. def get_last_reply_url(self):
  122. return self.get_url('last')
  123. def get_new_reply_url(self):
  124. return self.get_url('new')
  125. def get_moderated_url(self):
  126. return self.get_url('moderated')
  127. def get_reported_url(self):
  128. return self.get_url('reported')
  129. def set_title(self, title):
  130. self.title = title
  131. self.slug = slugify(title)
  132. def set_first_post(self, post):
  133. self.started_on = post.posted_on
  134. self.first_post = post
  135. self.starter = post.poster
  136. self.starter_name = post.poster_name
  137. if post.poster:
  138. self.starter_slug = post.poster.slug
  139. else:
  140. self.starter_slug = slugify(post.poster_name)
  141. self.is_moderated = post.is_moderated
  142. self.is_hidden = post.is_hidden
  143. def set_last_post(self, post):
  144. self.last_post_on = post.posted_on
  145. self.last_post = post
  146. self.last_poster = post.poster
  147. self.last_poster_name = post.poster_name
  148. if post.poster:
  149. self.last_poster_slug = post.poster.slug
  150. else:
  151. self.last_poster_slug = slugify(post.poster_name)
  152. class ThreadParticipantManager(models.Manager):
  153. def delete_participant(self, thread, user):
  154. ThreadParticipant.objects.filter(thread=thread, user=user).delete()
  155. @transaction.atomic
  156. def set_owner(self, thread, user):
  157. thread_owner = ThreadParticipant.objects.filter(
  158. thread=thread, level=PARTICIPANT_OWNER)
  159. thread_owner.update(level=PARTICIPANT_ACTIVE)
  160. self.delete_participant(thread, user)
  161. ThreadParticipant.objects.create(
  162. thread=thread,
  163. user=user,
  164. level=PARTICIPANT_OWNER)
  165. @transaction.atomic
  166. def add_participant(self, thread, user):
  167. self.delete_participant(thread, user)
  168. ThreadParticipant.objects.create(
  169. thread=thread,
  170. user=user,
  171. level=PARTICIPANT_ACTIVE)
  172. @transaction.atomic
  173. def remove_participant(self, thread, user):
  174. self.delete_participant(thread, user)
  175. ThreadParticipant.objects.create(
  176. thread=thread,
  177. user=user,
  178. level=PARTICIPANT_REMOVED,
  179. replies=thread.replies,
  180. last_post_on=thread.last_post_on,
  181. last_poster_id=thread.last_poster_id,
  182. last_poster_name=thread.last_poster_name,
  183. last_poster_slug=thread.last_poster_slug)
  184. class ThreadParticipant(models.Model):
  185. thread = models.ForeignKey(Thread)
  186. user = models.ForeignKey(settings.AUTH_USER_MODEL)
  187. level = models.PositiveIntegerField(default=PARTICIPANT_ACTIVE)
  188. replies = models.PositiveIntegerField(default=0)
  189. last_post_on = models.DateTimeField(null=True, blank=True)
  190. last_poster = models.ForeignKey(settings.AUTH_USER_MODEL,
  191. related_name='+',
  192. null=True, blank=True,
  193. on_delete=models.SET_NULL)
  194. last_poster_name = models.CharField(max_length=255, null=True, blank=True)
  195. last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
  196. objects = ThreadParticipantManager()
  197. @property
  198. def is_removed(self):
  199. return self.level == PARTICIPANT_REMOVED
  200. @property
  201. def is_active(self):
  202. return self.level == PARTICIPANT_ACTIVE
  203. @property
  204. def is_owner(self):
  205. return self.level == PARTICIPANT_OWNER