post.py 7.7 KB

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