__init__.py
369 lines
| 11.6 KiB
| text/x-python
|
PythonLexer
|
r1156 | import logging | ||
import re | ||||
import uuid | ||||
|
r1533 | from boards.utils import datetime_to_epoch | ||
|
r1156 | from django.core.exceptions import ObjectDoesNotExist | ||
from django.core.urlresolvers import reverse | ||||
|
r1359 | from django.db import models | ||
|
r1221 | from django.db.models import TextField, QuerySet | ||
|
r1358 | from django.template.defaultfilters import striptags, truncatewords | ||
|
r1156 | from django.template.loader import render_to_string | ||
from django.utils import timezone | ||||
|
r1498 | from django.db.models.signals import post_save, pre_save | ||
from django.dispatch import receiver | ||||
|
r1156 | |||
from boards import settings | ||||
|
r1296 | from boards.abstracts.tripcode import Tripcode | ||
|
r1499 | from boards.mdx_neboard import get_parser | ||
|
r1273 | from boards.models import PostImage, Attachment | ||
|
r1156 | from boards.models.base import Viewable | ||
from boards.models.post.export import get_exporter, DIFF_TYPE_JSON | ||||
|
r1359 | from boards.models.post.manager import PostManager | ||
from boards.models.user import Notification | ||||
|
r1156 | |||
|
r1366 | CSS_CLS_HIDDEN_POST = 'hidden_post' | ||
CSS_CLS_DEAD_POST = 'dead_post' | ||||
CSS_CLS_ARCHIVE_POST = 'archive_post' | ||||
CSS_CLS_POST = 'post' | ||||
|
r1434 | CSS_CLS_MONOCHROME = 'monochrome' | ||
|
r1366 | |||
|
r1358 | TITLE_MAX_WORDS = 10 | ||
|
r1156 | APP_LABEL_BOARDS = 'boards' | ||
BAN_REASON_AUTO = 'Auto' | ||||
IMAGE_THUMB_SIZE = (200, 150) | ||||
TITLE_MAX_LENGTH = 200 | ||||
REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]') | ||||
REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]') | ||||
PARAMETER_TRUNCATED = 'truncated' | ||||
PARAMETER_TAG = 'tag' | ||||
PARAMETER_OFFSET = 'offset' | ||||
PARAMETER_DIFF_TYPE = 'type' | ||||
PARAMETER_CSS_CLASS = 'css_class' | ||||
PARAMETER_THREAD = 'thread' | ||||
PARAMETER_IS_OPENING = 'is_opening' | ||||
PARAMETER_POST = 'post' | ||||
PARAMETER_OP_ID = 'opening_post_id' | ||||
PARAMETER_NEED_OPEN_LINK = 'need_open_link' | ||||
PARAMETER_REPLY_LINK = 'reply_link' | ||||
|
r1166 | PARAMETER_NEED_OP_DATA = 'need_op_data' | ||
|
r1156 | |||
|
r1180 | POST_VIEW_PARAMS = ( | ||
'need_op_data', | ||||
'reply_link', | ||||
'need_open_link', | ||||
'truncated', | ||||
'mode_tree', | ||||
|
r1388 | 'perms', | ||
|
r1473 | 'tree_depth', | ||
|
r1180 | ) | ||
|
r1358 | |||
|
r1156 | class Post(models.Model, Viewable): | ||
"""A post is a message.""" | ||||
objects = PostManager() | ||||
class Meta: | ||||
app_label = APP_LABEL_BOARDS | ||||
ordering = ('id',) | ||||
title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True) | ||||
pub_time = models.DateTimeField() | ||||
text = TextField(blank=True, null=True) | ||||
_text_rendered = TextField(blank=True, null=True, editable=False) | ||||
images = models.ManyToManyField(PostImage, null=True, blank=True, | ||||
|
r1250 | related_name='post_images', db_index=True) | ||
|
r1273 | attachments = models.ManyToManyField(Attachment, null=True, blank=True, | ||
|
r1358 | related_name='attachment_posts') | ||
|
r1156 | |||
poster_ip = models.GenericIPAddressField() | ||||
# TODO This field can be removed cause UID is used for update now | ||||
last_edit_time = models.DateTimeField() | ||||
referenced_posts = models.ManyToManyField('Post', symmetrical=False, | ||||
null=True, | ||||
|
r1180 | blank=True, related_name='refposts', | ||
|
r1156 | db_index=True) | ||
refmap = models.TextField(null=True, blank=True) | ||||
|
r1345 | threads = models.ManyToManyField('Thread', db_index=True, | ||
related_name='multi_replies') | ||||
|
r1156 | thread = models.ForeignKey('Thread', db_index=True, related_name='pt+') | ||
url = models.TextField() | ||||
uid = models.TextField(db_index=True) | ||||
|
r1367 | tripcode = models.CharField(max_length=50, blank=True, default='') | ||
|
r1381 | opening = models.BooleanField(db_index=True) | ||
|
r1366 | hidden = models.BooleanField(default=False) | ||
|
r1293 | |||
|
r1156 | def __str__(self): | ||
|
r1358 | return 'P#{}/{}'.format(self.id, self.get_title()) | ||
|
r1156 | |||
def get_title(self) -> str: | ||||
|
r1358 | return self.title | ||
|
r1156 | |||
|
r1358 | def get_title_or_text(self): | ||
title = self.get_title() | ||||
|
r1156 | if not title: | ||
|
r1358 | title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS) | ||
|
r1156 | |||
return title | ||||
def build_refmap(self) -> None: | ||||
""" | ||||
Builds a replies map string from replies list. This is a cache to stop | ||||
the server from recalculating the map on every post show. | ||||
""" | ||||
|
r1309 | post_urls = [refpost.get_link_view() | ||
|
r1156 | for refpost in self.referenced_posts.all()] | ||
self.refmap = ', '.join(post_urls) | ||||
def is_referenced(self) -> bool: | ||||
return self.refmap and len(self.refmap) > 0 | ||||
def is_opening(self) -> bool: | ||||
""" | ||||
Checks if this is an opening post or just a reply. | ||||
""" | ||||
|
r1337 | return self.opening | ||
|
r1156 | |||
|
r1365 | def get_absolute_url(self, thread=None): | ||
url = None | ||||
if thread is None: | ||||
thread = self.get_thread() | ||||
# Url is cached only for the "main" thread. When getting url | ||||
# for other threads, do it manually. | ||||
if self.url: | ||||
url = self.url | ||||
if url is None: | ||||
|
r1443 | opening = self.is_opening() | ||
opening_id = self.id if opening else thread.get_opening_post_id() | ||||
|
r1365 | url = reverse('thread', kwargs={'post_id': opening_id}) | ||
|
r1443 | if not opening: | ||
|
r1365 | url += '#' + str(self.id) | ||
return url | ||||
|
r1192 | |||
|
r1156 | def get_thread(self): | ||
return self.thread | ||||
|
r1455 | def get_thread_id(self): | ||
return self.thread_id | ||||
|
r1221 | def get_threads(self) -> QuerySet: | ||
|
r1156 | """ | ||
Gets post's thread. | ||||
""" | ||||
return self.threads | ||||
|
r1531 | def _get_cache_key(self): | ||
return [datetime_to_epoch(self.last_edit_time)] | ||||
|
r1180 | def get_view(self, *args, **kwargs) -> str: | ||
|
r1156 | """ | ||
Renders post's HTML view. Some of the post params can be passed over | ||||
kwargs for the means of caching (if we view the thread, some params | ||||
are same for every post and don't need to be computed over and over. | ||||
""" | ||||
thread = self.get_thread() | ||||
|
r1366 | css_classes = [CSS_CLS_POST] | ||
|
r1414 | if thread.is_archived(): | ||
|
r1366 | css_classes.append(CSS_CLS_ARCHIVE_POST) | ||
|
r1156 | elif not thread.can_bump(): | ||
|
r1366 | css_classes.append(CSS_CLS_DEAD_POST) | ||
if self.is_hidden(): | ||||
css_classes.append(CSS_CLS_HIDDEN_POST) | ||||
|
r1434 | if thread.is_monochrome(): | ||
css_classes.append(CSS_CLS_MONOCHROME) | ||||
|
r1156 | |||
|
r1180 | params = dict() | ||
for param in POST_VIEW_PARAMS: | ||||
if param in kwargs: | ||||
params[param] = kwargs[param] | ||||
params.update({ | ||||
|
r1156 | PARAMETER_POST: self, | ||
|
r1337 | PARAMETER_IS_OPENING: self.is_opening(), | ||
|
r1156 | PARAMETER_THREAD: thread, | ||
|
r1366 | PARAMETER_CSS_CLASS: ' '.join(css_classes), | ||
|
r1156 | }) | ||
|
r1180 | return render_to_string('boards/post.html', params) | ||
|
r1156 | def get_search_view(self, *args, **kwargs): | ||
|
r1172 | return self.get_view(need_op_data=True, *args, **kwargs) | ||
|
r1156 | |||
def get_first_image(self) -> PostImage: | ||||
return self.images.earliest('id') | ||||
def delete(self, using=None): | ||||
""" | ||||
Deletes all post images and the post itself. | ||||
""" | ||||
for image in self.images.all(): | ||||
|
r1281 | image_refs_count = image.post_images.count() | ||
|
r1156 | if image_refs_count == 1: | ||
image.delete() | ||||
|
r1278 | for attachment in self.attachments.all(): | ||
|
r1281 | attachment_refs_count = attachment.attachment_posts.count() | ||
|
r1278 | if attachment_refs_count == 1: | ||
attachment.delete() | ||||
|
r1156 | thread = self.get_thread() | ||
thread.last_edit_time = timezone.now() | ||||
thread.save() | ||||
super(Post, self).delete(using) | ||||
logging.getLogger('boards.post.delete').info( | ||||
'Deleted post {}'.format(self)) | ||||
def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None, | ||||
include_last_update=False) -> str: | ||||
""" | ||||
Gets post HTML or JSON data that can be rendered on a page or used by | ||||
API. | ||||
""" | ||||
return get_exporter(format_type).export(self, request, | ||||
include_last_update) | ||||
def notify_clients(self, recursive=True): | ||||
""" | ||||
Sends post HTML data to the thread web socket. | ||||
""" | ||||
if not settings.get_bool('External', 'WebsocketsEnabled'): | ||||
return | ||||
thread_ids = list() | ||||
for thread in self.get_threads().all(): | ||||
thread_ids.append(thread.id) | ||||
thread.notify_clients() | ||||
if recursive: | ||||
for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()): | ||||
post_id = reply_number.group(1) | ||||
try: | ||||
ref_post = Post.objects.get(id=post_id) | ||||
if ref_post.get_threads().exclude(id__in=thread_ids).exists(): | ||||
# If post is in this thread, its thread was already notified. | ||||
# Otherwise, notify its thread separately. | ||||
ref_post.notify_clients(recursive=False) | ||||
except ObjectDoesNotExist: | ||||
pass | ||||
def build_url(self): | ||||
|
r1192 | self.url = self.get_absolute_url() | ||
|
r1156 | self.save(update_fields=['url']) | ||
def save(self, force_insert=False, force_update=False, using=None, | ||||
update_fields=None): | ||||
|
r1440 | new_post = self.id is None | ||
|
r1156 | self.uid = str(uuid.uuid4()) | ||
if update_fields is not None and 'uid' not in update_fields: | ||||
update_fields += ['uid'] | ||||
|
r1440 | if not new_post: | ||
|
r1156 | for thread in self.get_threads().all(): | ||
thread.last_edit_time = self.last_edit_time | ||||
|
r1414 | thread.save(update_fields=['last_edit_time', 'status']) | ||
|
r1156 | |||
super().save(force_insert, force_update, using, update_fields) | ||||
|
r1443 | if self.url is None: | ||
|
r1440 | self.build_url() | ||
|
r1156 | def get_text(self) -> str: | ||
return self._text_rendered | ||||
def get_raw_text(self) -> str: | ||||
return self.text | ||||
def connect_threads(self, opening_posts): | ||||
for opening_post in opening_posts: | ||||
threads = opening_post.get_threads().all() | ||||
for thread in threads: | ||||
if thread.can_bump(): | ||||
thread.update_bump_status() | ||||
thread.last_edit_time = self.last_edit_time | ||||
|
r1414 | thread.save(update_fields=['last_edit_time', 'status']) | ||
|
r1201 | self.threads.add(opening_post.get_thread()) | ||
|
r1293 | |||
|
r1296 | def get_tripcode(self): | ||
if self.tripcode: | ||||
return Tripcode(self.tripcode) | ||||
|
r1309 | |||
def get_link_view(self): | ||||
""" | ||||
Gets view of a reflink to the post. | ||||
""" | ||||
result = '<a href="{}">>>{}</a>'.format(self.get_absolute_url(), | ||||
|
r1323 | self.id) | ||
|
r1309 | if self.is_opening(): | ||
result = '<b>{}</b>'.format(result) | ||||
return result | ||||
|
r1366 | |||
def is_hidden(self) -> bool: | ||||
return self.hidden | ||||
def set_hidden(self, hidden): | ||||
self.hidden = hidden | ||||
|
r1498 | |||
# SIGNALS (Maybe move to other module?) | ||||
@receiver(post_save, sender=Post) | ||||
def connect_replies(instance, **kwargs): | ||||
for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()): | ||||
post_id = reply_number.group(1) | ||||
try: | ||||
referenced_post = Post.objects.get(id=post_id) | ||||
|
r1542 | # Connect only to posts that are not connected to already | ||
if not referenced_post.referenced_posts.filter(id=instance.id).exists(): | ||||
referenced_post.referenced_posts.add(instance) | ||||
referenced_post.last_edit_time = instance.pub_time | ||||
referenced_post.build_refmap() | ||||
referenced_post.save(update_fields=['refmap', 'last_edit_time']) | ||||
|
r1498 | except ObjectDoesNotExist: | ||
pass | ||||
@receiver(post_save, sender=Post) | ||||
def connect_notifications(instance, **kwargs): | ||||
for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()): | ||||
user_name = reply_number.group(1).lower() | ||||
Notification.objects.get_or_create(name=user_name, post=instance) | ||||
@receiver(pre_save, sender=Post) | ||||
def preparse_text(instance, **kwargs): | ||||
|
r1499 | instance._text_rendered = get_parser().parse(instance.get_raw_text()) | ||