post.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import copy
  2. from django.contrib.postgres.indexes import GinIndex
  3. from django.contrib.postgres.fields import JSONField
  4. from django.contrib.postgres.search import SearchVector, SearchVectorField
  5. from django.db import models
  6. from django.utils import timezone
  7. from misago.conf import settings
  8. from misago.core.pgutils import PgPartialIndex
  9. from misago.core.utils import parse_iso8601_string
  10. from misago.markup import finalise_markup
  11. from misago.threads.checksums import is_post_valid, update_post_checksum
  12. from misago.threads.filtersearch import filter_search
  13. class Post(models.Model):
  14. category = models.ForeignKey(
  15. 'misago_categories.Category',
  16. on_delete=models.CASCADE,
  17. )
  18. thread = models.ForeignKey(
  19. 'misago_threads.Thread',
  20. on_delete=models.CASCADE,
  21. )
  22. poster = models.ForeignKey(
  23. settings.AUTH_USER_MODEL,
  24. blank=True,
  25. null=True,
  26. on_delete=models.SET_NULL,
  27. )
  28. poster_name = models.CharField(max_length=255)
  29. original = models.TextField()
  30. parsed = models.TextField()
  31. checksum = models.CharField(max_length=64, default='-')
  32. mentions = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="mention_set")
  33. attachments_cache = JSONField(null=True, blank=True)
  34. posted_on = models.DateTimeField(db_index=True)
  35. updated_on = models.DateTimeField()
  36. hidden_on = models.DateTimeField(default=timezone.now)
  37. edits = models.PositiveIntegerField(default=0)
  38. last_editor = models.ForeignKey(
  39. settings.AUTH_USER_MODEL,
  40. blank=True,
  41. null=True,
  42. on_delete=models.SET_NULL,
  43. related_name='+',
  44. )
  45. last_editor_name = models.CharField(max_length=255, null=True, blank=True)
  46. last_editor_slug = models.SlugField(max_length=255, null=True, blank=True)
  47. hidden_by = models.ForeignKey(
  48. settings.AUTH_USER_MODEL,
  49. blank=True,
  50. null=True,
  51. on_delete=models.SET_NULL,
  52. related_name='+',
  53. )
  54. hidden_by_name = models.CharField(max_length=255, null=True, blank=True)
  55. hidden_by_slug = models.SlugField(max_length=255, null=True, blank=True)
  56. has_reports = models.BooleanField(default=False)
  57. has_open_reports = models.BooleanField(default=False)
  58. is_unapproved = models.BooleanField(default=False, db_index=True)
  59. is_hidden = models.BooleanField(default=False)
  60. is_protected = models.BooleanField(default=False)
  61. is_event = models.BooleanField(default=False, db_index=True)
  62. event_type = models.CharField(max_length=255, null=True, blank=True)
  63. event_context = JSONField(null=True, blank=True)
  64. likes = models.PositiveIntegerField(default=0)
  65. last_likes = JSONField(null=True, blank=True)
  66. liked_by = models.ManyToManyField(
  67. settings.AUTH_USER_MODEL,
  68. related_name='liked_post_set',
  69. through='misago_threads.PostLike',
  70. )
  71. search_document = models.TextField(null=True, blank=True)
  72. search_vector = SearchVectorField()
  73. class Meta:
  74. indexes = [
  75. PgPartialIndex(
  76. fields=['has_open_reports'],
  77. where={'has_open_reports': True},
  78. ),
  79. PgPartialIndex(
  80. fields=['is_hidden'],
  81. where={'is_hidden': False},
  82. ),
  83. PgPartialIndex(
  84. fields=['is_event', 'event_type'],
  85. where={'is_event': True},
  86. ),
  87. GinIndex(fields=['search_vector']),
  88. ]
  89. index_together = [
  90. ('thread', 'id'), # speed up threadview for team members
  91. ('is_event', 'is_hidden'),
  92. ('poster', 'posted_on'),
  93. ]
  94. def __str__(self):
  95. return '%s...' % self.original[10:].strip()
  96. def delete(self, *args, **kwargs):
  97. from misago.threads.signals import delete_post
  98. delete_post.send(sender=self)
  99. super(Post, self).delete(*args, **kwargs)
  100. def merge(self, other_post):
  101. if self.poster_id != other_post.poster_id:
  102. raise ValueError("post can't be merged with other user's post")
  103. elif (self.poster_id is None and other_post.poster_id is None and
  104. self.poster_name != other_post.poster_name):
  105. raise ValueError("post can't be merged with other user's post")
  106. if self.thread_id != other_post.thread_id:
  107. raise ValueError("only posts belonging to same thread can be merged")
  108. if self.is_event or other_post.is_event:
  109. raise ValueError("can't merge events")
  110. if self.pk == other_post.pk:
  111. raise ValueError("post can't be merged with itself")
  112. other_post.original = str('\n\n').join((other_post.original, self.original))
  113. other_post.parsed = str('\n').join((other_post.parsed, self.parsed))
  114. update_post_checksum(other_post)
  115. if self.is_protected:
  116. other_post.is_protected = True
  117. if self.is_best_answer:
  118. self.thread.best_answer = other_post
  119. if other_post.is_best_answer:
  120. self.thread.best_answer_is_protected = other_post.is_protected
  121. from misago.threads.signals import merge_post
  122. merge_post.send(sender=self, other_post=other_post)
  123. def move(self, new_thread):
  124. from misago.threads.signals import move_post
  125. if self.is_best_answer:
  126. self.thread.clear_best_answer()
  127. self.category = new_thread.category
  128. self.thread = new_thread
  129. move_post.send(sender=self)
  130. @property
  131. def attachments(self):
  132. if hasattr(self, '_hydrated_attachments_cache'):
  133. return self._hydrated_attachments_cache
  134. self._hydrated_attachments_cache = []
  135. if self.attachments_cache:
  136. for attachment in copy.deepcopy(self.attachments_cache):
  137. attachment['uploaded_on'] = parse_iso8601_string(attachment['uploaded_on'])
  138. self._hydrated_attachments_cache.append(attachment)
  139. return self._hydrated_attachments_cache
  140. @property
  141. def content(self):
  142. if not hasattr(self, '_finalised_parsed'):
  143. self._finalised_parsed = finalise_markup(self.parsed)
  144. return self._finalised_parsed
  145. @property
  146. def thread_type(self):
  147. return self.category.thread_type
  148. def get_api_url(self):
  149. return self.thread_type.get_post_api_url(self)
  150. def get_likes_api_url(self):
  151. return self.thread_type.get_post_likes_api_url(self)
  152. def get_editor_api_url(self):
  153. return self.thread_type.get_post_editor_api_url(self)
  154. def get_edits_api_url(self):
  155. return self.thread_type.get_post_edits_api_url(self)
  156. def get_read_api_url(self):
  157. return self.thread_type.get_post_read_api_url(self)
  158. def get_absolute_url(self):
  159. return self.thread_type.get_post_absolute_url(self)
  160. def set_search_document(self, thread_title=None):
  161. if thread_title:
  162. self.search_document = filter_search('\n\n'.join([thread_title, self.original]))
  163. else:
  164. self.search_document = filter_search(self.original)
  165. def update_search_vector(self):
  166. self.search_vector = SearchVector(
  167. 'search_document',
  168. config=settings.MISAGO_SEARCH_CONFIG,
  169. )
  170. @property
  171. def short(self):
  172. if self.is_valid:
  173. if len(self.original) > 150:
  174. return str('%s...') % self.original[:150].strip()
  175. else:
  176. return self.original
  177. else:
  178. return ''
  179. @property
  180. def is_valid(self):
  181. return is_post_valid(self)
  182. @property
  183. def is_first_post(self):
  184. return self.id == self.thread.first_post_id
  185. @property
  186. def is_best_answer(self):
  187. return self.id == self.thread.best_answer_id