from __future__ import unicode_literals import copy from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.fields import JSONField from django.contrib.postgres.search import SearchVector, SearchVectorField from django.db import models from django.utils import six, timezone from django.utils.encoding import python_2_unicode_compatible from misago.conf import settings from misago.core.pgutils import PgPartialIndex from misago.core.utils import parse_iso8601_string from misago.markup import finalise_markup from misago.threads.checksums import is_post_valid, update_post_checksum from misago.threads.filtersearch import filter_search @python_2_unicode_compatible class Post(models.Model): category = models.ForeignKey( 'misago_categories.Category', on_delete=models.CASCADE, ) thread = models.ForeignKey( 'misago_threads.Thread', on_delete=models.CASCADE, ) poster = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, ) poster_name = models.CharField(max_length=255) original = models.TextField() parsed = models.TextField() checksum = models.CharField(max_length=64, default='-') mentions = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="mention_set") attachments_cache = JSONField(null=True, blank=True) posted_on = models.DateTimeField(db_index=True) updated_on = models.DateTimeField() hidden_on = models.DateTimeField(default=timezone.now) edits = models.PositiveIntegerField(default=0) last_editor = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', ) last_editor_name = models.CharField(max_length=255, null=True, blank=True) last_editor_slug = models.SlugField(max_length=255, null=True, blank=True) hidden_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', ) hidden_by_name = models.CharField(max_length=255, null=True, blank=True) hidden_by_slug = models.SlugField(max_length=255, null=True, blank=True) has_reports = models.BooleanField(default=False) has_open_reports = models.BooleanField(default=False) is_unapproved = models.BooleanField(default=False, db_index=True) is_hidden = models.BooleanField(default=False) is_protected = models.BooleanField(default=False) is_event = models.BooleanField(default=False, db_index=True) event_type = models.CharField(max_length=255, null=True, blank=True) event_context = JSONField(null=True, blank=True) likes = models.PositiveIntegerField(default=0) last_likes = JSONField(null=True, blank=True) liked_by = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name='liked_post_set', through='misago_threads.PostLike', ) search_document = models.TextField(null=True, blank=True) search_vector = SearchVectorField() class Meta: indexes = [ PgPartialIndex( fields=['has_open_reports'], where={'has_open_reports': True}, ), PgPartialIndex( fields=['is_hidden'], where={'is_hidden': False}, ), PgPartialIndex( fields=['is_event', 'event_type'], where={'is_event': True}, ), GinIndex(fields=['search_vector']), ] index_together = [ ('thread', 'id'), # speed up threadview for team members ('is_event', 'is_hidden'), ('poster', 'posted_on'), ] def __str__(self): return '%s...' % self.original[10:].strip() def delete(self, *args, **kwargs): from misago.threads.signals import delete_post delete_post.send(sender=self) super(Post, self).delete(*args, **kwargs) def merge(self, other_post): if self.poster_id != other_post.poster_id: raise ValueError("post can't be merged with other user's post") elif (self.poster_id is None and other_post.poster_id is None and self.poster_name != other_post.poster_name): raise ValueError("post can't be merged with other user's post") if self.thread_id != other_post.thread_id: raise ValueError("only posts belonging to same thread can be merged") if self.is_event or other_post.is_event: raise ValueError("can't merge events") if self.pk == other_post.pk: raise ValueError("post can't be merged with itself") other_post.original = six.text_type('\n\n').join((other_post.original, self.original)) other_post.parsed = six.text_type('\n').join((other_post.parsed, self.parsed)) update_post_checksum(other_post) if self.is_protected: other_post.is_protected = True if self.is_best_answer: self.thread.best_answer = other_post if other_post.is_best_answer: self.thread.best_answer_is_protected = other_post.is_protected from misago.threads.signals import merge_post merge_post.send(sender=self, other_post=other_post) def move(self, new_thread): from misago.threads.signals import move_post if self.is_best_answer: self.thread.clear_best_answer() self.category = new_thread.category self.thread = new_thread move_post.send(sender=self) @property def attachments(self): if hasattr(self, '_hydrated_attachments_cache'): return self._hydrated_attachments_cache self._hydrated_attachments_cache = [] if self.attachments_cache: for attachment in copy.deepcopy(self.attachments_cache): attachment['uploaded_on'] = parse_iso8601_string(attachment['uploaded_on']) self._hydrated_attachments_cache.append(attachment) return self._hydrated_attachments_cache @property def content(self): if not hasattr(self, '_finalised_parsed'): self._finalised_parsed = finalise_markup(self.parsed) return self._finalised_parsed @property def thread_type(self): return self.category.thread_type def get_api_url(self): return self.thread_type.get_post_api_url(self) def get_likes_api_url(self): return self.thread_type.get_post_likes_api_url(self) def get_editor_api_url(self): return self.thread_type.get_post_editor_api_url(self) def get_edits_api_url(self): return self.thread_type.get_post_edits_api_url(self) def get_read_api_url(self): return self.thread_type.get_post_read_api_url(self) def get_absolute_url(self): return self.thread_type.get_post_absolute_url(self) def set_search_document(self, thread_title=None): if thread_title: self.search_document = filter_search('\n\n'.join([thread_title, self.original])) else: self.search_document = filter_search(self.original) def update_search_vector(self): self.search_vector = SearchVector( 'search_document', config=settings.MISAGO_SEARCH_CONFIG, ) @property def short(self): if self.is_valid: if len(self.original) > 150: return six.text_type('%s...') % self.original[:150].strip() else: return self.original else: return '' @property def is_valid(self): return is_post_valid(self) @property def is_first_post(self): return self.id == self.thread.first_post_id @property def is_best_answer(self): return self.id == self.thread.best_answer_id