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.core.urlresolvers import reverse
  5. from django.db import models
  6. from django.dispatch import receiver
  7. from django.utils.encoding import python_2_unicode_compatible
  8. from django.utils import six, timezone
  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