post.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from __future__ import unicode_literals
  2. import copy
  3. from django.contrib.postgres.fields import JSONField
  4. from django.db import models
  5. from django.dispatch import receiver
  6. from django.urls import reverse
  7. from django.utils import six, timezone
  8. from django.utils.encoding import python_2_unicode_compatible
  9. from misago.conf import settings
  10. from misago.core.utils import parse_iso8601_string
  11. from misago.markup import finalise_markup
  12. from .. import threadtypes
  13. from ..checksums import is_post_valid, update_post_checksum
  14. @python_2_unicode_compatible
  15. class Post(models.Model):
  16. category = models.ForeignKey('misago_categories.Category')
  17. thread = models.ForeignKey('misago_threads.Thread')
  18. poster = models.ForeignKey(
  19. settings.AUTH_USER_MODEL,
  20. blank=True,
  21. null=True,
  22. on_delete=models.SET_NULL,
  23. )
  24. poster_name = models.CharField(max_length=255)
  25. poster_ip = models.GenericIPAddressField()
  26. original = models.TextField()
  27. parsed = models.TextField()
  28. checksum = models.CharField(max_length=64, default='-')
  29. mentions = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="mention_set")
  30. attachments_cache = JSONField(null=True, blank=True)
  31. posted_on = models.DateTimeField()
  32. updated_on = models.DateTimeField()
  33. hidden_on = models.DateTimeField(default=timezone.now)
  34. edits = models.PositiveIntegerField(default=0)
  35. last_editor = models.ForeignKey(
  36. settings.AUTH_USER_MODEL,
  37. blank=True,
  38. null=True,
  39. on_delete=models.SET_NULL,
  40. related_name='+',
  41. )
  42. last_editor_name = models.CharField(max_length=255, null=True, blank=True)
  43. last_editor_slug = models.SlugField(max_length=255, null=True, blank=True)
  44. hidden_by = models.ForeignKey(
  45. settings.AUTH_USER_MODEL,
  46. blank=True,
  47. null=True,
  48. on_delete=models.SET_NULL,
  49. related_name='+',
  50. )
  51. hidden_by_name = models.CharField(max_length=255, null=True, blank=True)
  52. hidden_by_slug = models.SlugField(max_length=255, null=True, blank=True)
  53. has_reports = models.BooleanField(default=False)
  54. has_open_reports = models.BooleanField(default=False)
  55. is_unapproved = models.BooleanField(default=False, db_index=True)
  56. is_hidden = models.BooleanField(default=False)
  57. is_protected = models.BooleanField(default=False)
  58. is_event = models.BooleanField(default=False, db_index=True)
  59. event_type = models.CharField(max_length=255, null=True, blank=True)
  60. event_context = JSONField(null=True, blank=True)
  61. likes = models.PositiveIntegerField(default=0)
  62. last_likes = JSONField(null=True, blank=True)
  63. liked_by = models.ManyToManyField(
  64. settings.AUTH_USER_MODEL,
  65. related_name='liked_post_set',
  66. through='misago_threads.PostLike',
  67. )
  68. class Meta:
  69. index_together = [
  70. ('thread', 'id'), # speed up threadview for team members
  71. ('is_event', 'is_hidden'),
  72. ('poster', 'posted_on')
  73. ]
  74. def __str__(self):
  75. return '%s...' % self.original[10:].strip()
  76. def delete(self, *args, **kwargs):
  77. from ..signals import delete_post
  78. delete_post.send(sender=self)
  79. super(Post, self).delete(*args, **kwargs)
  80. def merge(self, other_post):
  81. if self.thread_id != other_post.thread_id:
  82. raise ValueError("only posts belonging to same thread can be merged")
  83. if self.is_event or other_post.is_event:
  84. raise ValueError("can't merge events")
  85. if self.pk == other_post.pk:
  86. raise ValueError("post can't be merged with itself")
  87. other_post.original = six.text_type('\n\n').join((other_post.original, self.original))
  88. other_post.parsed = six.text_type('\n').join((other_post.parsed, self.parsed))
  89. update_post_checksum(other_post)
  90. from ..signals import merge_post
  91. merge_post.send(sender=self, other_post=other_post)
  92. def move(self, new_thread):
  93. from ..signals import move_post
  94. self.category = new_thread.category
  95. self.thread = new_thread
  96. move_post.send(sender=self)
  97. @property
  98. def attachments(self):
  99. if hasattr(self, '_hydrated_attachments_cache'):
  100. return self._hydrated_attachments_cache
  101. self._hydrated_attachments_cache = []
  102. if self.attachments_cache:
  103. for attachment in copy.deepcopy(self.attachments_cache):
  104. attachment['uploaded_on'] = parse_iso8601_string(attachment['uploaded_on'])
  105. self._hydrated_attachments_cache.append(attachment)
  106. return self._hydrated_attachments_cache
  107. @property
  108. def content(self):
  109. if not hasattr(self, '_finalised_parsed'):
  110. self._finalised_parsed = finalise_markup(self.parsed)
  111. return self._finalised_parsed
  112. @property
  113. def thread_type(self):
  114. return self.category.thread_type
  115. def get_api_url(self):
  116. return self.thread_type.get_post_api_url(self)
  117. def get_likes_api_url(self):
  118. return self.thread_type.get_post_likes_api_url(self)
  119. def get_editor_api_url(self):
  120. return self.thread_type.get_post_editor_api_url(self)
  121. def get_edits_api_url(self):
  122. return self.thread_type.get_post_edits_api_url(self)
  123. def get_read_api_url(self):
  124. return self.thread_type.get_post_read_api_url(self)
  125. def get_absolute_url(self):
  126. return self.thread_type.get_post_absolute_url(self)
  127. @property
  128. def short(self):
  129. if self.is_valid:
  130. if len(self.original) > 150:
  131. return six.text_type('%s...') % self.original[:150].strip()
  132. else:
  133. return self.original
  134. else:
  135. return ''
  136. @property
  137. def is_valid(self):
  138. return is_post_valid(self)
  139. @property
  140. def is_first_post(self):
  141. return self.pk == self.thread.first_post_id