post.py 5.5 KB

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