##// END OF EJS Templates
Merged with default branch
Merged with default branch

File last commit:

r1501:c4d18abb merge decentral
r1501:c4d18abb merge decentral
Show More
__init__.py
452 lines | 14.2 KiB | text/x-python | PythonLexer
neko259
Refactored post export
r1156 import logging
import re
import uuid
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
neko259
Moved post manager in a separate module
r1359 from django.db import models
neko259
Fixed thread bumping
r1221 from django.db.models import TextField, QuerySet
neko259
Fixed import of truncatewords
r1385 from django.template.defaultfilters import truncatewords, striptags
neko259
Refactored post export
r1156 from django.template.loader import render_to_string
from django.utils import timezone
from boards import settings
neko259
Added ability to reset tripcode
r1296 from boards.abstracts.tripcode import Tripcode
neko259
Use singleton parser
r1499 from boards.mdx_neboard import get_parser
neko259
Merged with default branch
r1360 from boards.models import PostImage, Attachment, KeyPair, GlobalId
neko259
Refactored post export
r1156 from boards.models.base import Viewable
from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
neko259
Moved post manager in a separate module
r1359 from boards.models.post.manager import PostManager
from boards.models.user import Notification
neko259
Refactored post export
r1156
neko259
Added ability to hide a post
r1366 CSS_CLS_HIDDEN_POST = 'hidden_post'
CSS_CLS_DEAD_POST = 'dead_post'
CSS_CLS_ARCHIVE_POST = 'archive_post'
CSS_CLS_POST = 'post'
neko259
Added ability to create monochrome threads
r1434 CSS_CLS_MONOCHROME = 'monochrome'
neko259
Added ability to hide a post
r1366
neko259
Unified getting post title or text part if there is no title
r1358 TITLE_MAX_WORDS = 10
neko259
Refactored post export
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\]')
neko259
Partly merged with default branch
r1157 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
neko259
Refactored post export
r1156 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'
neko259
Add link to thread in the post feed
r1166 PARAMETER_NEED_OP_DATA = 'need_op_data'
neko259
Refactored post export
r1156
neko259
Added tree mode for the thread
r1180 POST_VIEW_PARAMS = (
'need_op_data',
'reply_link',
'need_open_link',
'truncated',
'mode_tree',
neko259
Use native django permission system in templates
r1388 'perms',
neko259
New fast recursion-free tree render engine
r1473 'tree_depth',
neko259
Added tree mode for the thread
r1180 )
neko259
Unified getting post title or text part if there is no title
r1358
neko259
Refactored post export
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,
neko259
Don't include archived posts into the random image list
r1250 related_name='post_images', db_index=True)
neko259
Added support for different attachment types
r1273 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
neko259
Unified getting post title or text part if there is no title
r1358 related_name='attachment_posts')
neko259
Refactored post export
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,
neko259
Added tree mode for the thread
r1180 blank=True, related_name='refposts',
neko259
Refactored post export
r1156 db_index=True)
refmap = models.TextField(null=True, blank=True)
neko259
Faster way of obtaining new post count
r1345 threads = models.ManyToManyField('Thread', db_index=True,
related_name='multi_replies')
neko259
Refactored post export
r1156 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
url = models.TextField()
uid = models.TextField(db_index=True)
neko259
Partly merged with default branch
r1157 # Global ID with author key. If the message was downloaded from another
# server, this indicates the server.
global_id = models.OneToOneField('GlobalId', null=True, blank=True)
neko259
Allow to edit tag and post in the admin site
r1367 tripcode = models.CharField(max_length=50, blank=True, default='')
neko259
Added index on post.opening. Use it when getting OP of a thread
r1381 opening = models.BooleanField(db_index=True)
neko259
Added ability to hide a post
r1366 hidden = models.BooleanField(default=False)
neko259
Added tripcodes
r1293
neko259
Refactored post export
r1156 def __str__(self):
neko259
Unified getting post title or text part if there is no title
r1358 return 'P#{}/{}'.format(self.id, self.get_title())
neko259
Refactored post export
r1156
neko259
Added tree mode for the thread
r1180 def get_referenced_posts(self):
neko259
Don't show replies from other threads in the tree mode
r1181 threads = self.get_threads().all()
return self.referenced_posts.filter(threads__in=threads)\
neko259
Unified getting post title or text part if there is no title
r1358 .order_by('pub_time').distinct().all()
neko259
Added tree mode for the thread
r1180
neko259
Refactored post export
r1156 def get_title(self) -> str:
neko259
Unified getting post title or text part if there is no title
r1358 return self.title
neko259
Refactored post export
r1156
neko259
Unified getting post title or text part if there is no title
r1358 def get_title_or_text(self):
title = self.get_title()
neko259
Refactored post export
r1156 if not title:
neko259
Unified getting post title or text part if there is no title
r1358 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
neko259
Refactored post export
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.
"""
neko259
Reflinks to OPs are bold now. Refactored reflinks to build using the same code. Refactored autoescaping
r1309 post_urls = [refpost.get_link_view()
neko259
Refactored post export
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.
"""
neko259
Save "opening post" flag with the post itself and don't count it every time. Speed up getting posts with attachments and images
r1337 return self.opening
neko259
Refactored post export
r1156
neko259
Show updated thread's link in favorite's new multipost
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:
neko259
Speed up getting post URL and made it work when OP is just created
r1443 opening = self.is_opening()
opening_id = self.id if opening else thread.get_opening_post_id()
neko259
Show updated thread's link in favorite's new multipost
r1365 url = reverse('thread', kwargs={'post_id': opening_id})
neko259
Speed up getting post URL and made it work when OP is just created
r1443 if not opening:
neko259
Show updated thread's link in favorite's new multipost
r1365 url += '#' + str(self.id)
return url
neko259
Fixed building reflink maps that were broken when post link generator was...
r1192
neko259
Refactored post export
r1156 def get_thread(self):
return self.thread
neko259
Speed up getting new post count for favorites. Get favorites at page start, not only by JS
r1455 def get_thread_id(self):
return self.thread_id
neko259
Fixed thread bumping
r1221 def get_threads(self) -> QuerySet:
neko259
Refactored post export
r1156 """
Gets post's thread.
"""
return self.threads
neko259
Added tree mode for the thread
r1180 def get_view(self, *args, **kwargs) -> str:
neko259
Refactored post export
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()
neko259
Added ability to hide a post
r1366 css_classes = [CSS_CLS_POST]
neko259
Thread status field instead of bumpable and archived fields (per BB-73)
r1414 if thread.is_archived():
neko259
Added ability to hide a post
r1366 css_classes.append(CSS_CLS_ARCHIVE_POST)
neko259
Refactored post export
r1156 elif not thread.can_bump():
neko259
Added ability to hide a post
r1366 css_classes.append(CSS_CLS_DEAD_POST)
if self.is_hidden():
css_classes.append(CSS_CLS_HIDDEN_POST)
neko259
Added ability to create monochrome threads
r1434 if thread.is_monochrome():
css_classes.append(CSS_CLS_MONOCHROME)
neko259
Refactored post export
r1156
neko259
Added tree mode for the thread
r1180 params = dict()
for param in POST_VIEW_PARAMS:
if param in kwargs:
params[param] = kwargs[param]
params.update({
neko259
Refactored post export
r1156 PARAMETER_POST: self,
neko259
Save "opening post" flag with the post itself and don't count it every time. Speed up getting posts with attachments and images
r1337 PARAMETER_IS_OPENING: self.is_opening(),
neko259
Refactored post export
r1156 PARAMETER_THREAD: thread,
neko259
Added ability to hide a post
r1366 PARAMETER_CSS_CLASS: ' '.join(css_classes),
neko259
Refactored post export
r1156 })
neko259
Added tree mode for the thread
r1180 return render_to_string('boards/post.html', params)
neko259
Refactored post export
r1156 def get_search_view(self, *args, **kwargs):
neko259
Show thread link and title in the search results and notifications
r1172 return self.get_view(need_op_data=True, *args, **kwargs)
neko259
Refactored post export
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():
neko259
Check post-images and post-attachments relations by related name
r1281 image_refs_count = image.post_images.count()
neko259
Refactored post export
r1156 if image_refs_count == 1:
image.delete()
neko259
Delete attachments with posts
r1278 for attachment in self.attachments.all():
neko259
Check post-images and post-attachments relations by related name
r1281 attachment_refs_count = attachment.attachment_posts.count()
neko259
Delete attachments with posts
r1278 if attachment_refs_count == 1:
attachment.delete()
neko259
Partly merged with default branch
r1157 if self.global_id:
self.global_id.delete()
neko259
Refactored post export
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))
neko259
Partly merged with default branch
r1157 def set_global_id(self, key_pair=None):
"""
Sets global id based on the given key pair. If no key pair is given,
default one is used.
"""
if key_pair:
key = key_pair
else:
try:
key = KeyPair.objects.get(primary=True)
except KeyPair.DoesNotExist:
# Do not update the global id because there is no key defined
return
global_id = GlobalId(key_type=key.key_type,
key=key.public_key,
neko259
Sync-import of a single post is working
r1229 local_id=self.id)
neko259
Partly merged with default branch
r1157 global_id.save()
self.global_id = global_id
self.save(update_fields=['global_id'])
neko259
Sync-import of a single post is working
r1229 def get_pub_time_str(self):
return str(self.pub_time)
neko259
Partly merged with default branch
r1157
def get_replied_ids(self):
"""
Gets ID list of the posts that this post replies.
"""
raw_text = self.get_raw_text()
local_replied = REGEX_REPLY.findall(raw_text)
global_replied = []
for match in REGEX_GLOBAL_REPLY.findall(raw_text):
key_type = match[0]
key = match[1]
local_id = match[2]
try:
global_id = GlobalId.objects.get(key_type=key_type,
neko259
Merged with defaul
r1241 key=key, local_id=local_id)
neko259
Partly merged with default branch
r1157 for post in Post.objects.filter(global_id=global_id).only('id'):
global_replied.append(post.id)
except GlobalId.DoesNotExist:
pass
return local_replied + global_replied
neko259
Refactored post export
r1156 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):
neko259
Fixed building reflink maps that were broken when post link generator was...
r1192 self.url = self.get_absolute_url()
neko259
Refactored post export
r1156 self.save(update_fields=['url'])
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
neko259
Refactored post post-save triggers
r1440 new_post = self.id is None
neko259
Refactored post export
r1156 self.uid = str(uuid.uuid4())
if update_fields is not None and 'uid' not in update_fields:
update_fields += ['uid']
neko259
Refactored post post-save triggers
r1440 if not new_post:
neko259
Refactored post export
r1156 for thread in self.get_threads().all():
thread.last_edit_time = self.last_edit_time
neko259
Thread status field instead of bumpable and archived fields (per BB-73)
r1414 thread.save(update_fields=['last_edit_time', 'status'])
neko259
Refactored post export
r1156
super().save(force_insert, force_update, using, update_fields)
neko259
Speed up getting post URL and made it work when OP is just created
r1443 if self.url is None:
neko259
Refactored post post-save triggers
r1440 self.build_url()
neko259
Refactored post export
r1156 def get_text(self) -> str:
return self._text_rendered
def get_raw_text(self) -> str:
return self.text
neko259
Convert local post IDs to global when generating responses
r1228 def get_sync_text(self) -> str:
"""
Returns text applicable for sync. It has absolute post reflinks.
"""
replacements = dict()
for post_id in REGEX_REPLY.findall(self.get_raw_text()):
absolute_post_id = str(Post.objects.get(id=post_id).global_id)
replacements[post_id] = absolute_post_id
text = self.get_raw_text()
for key in replacements:
text = text.replace('[post]{}[/post]'.format(key),
'[post]{}[/post]'.format(replacements[key]))
return text
neko259
Refactored post export
r1156 def get_absolute_id(self) -> str:
"""
If the post has many threads, shows its main thread OP id in the post
ID.
"""
if self.get_threads().count() > 1:
return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
else:
return str(self.id)
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
neko259
Thread status field instead of bumpable and archived fields (per BB-73)
r1414 thread.save(update_fields=['last_edit_time', 'status'])
neko259
Connect reply to the multi-thread OP only to this OP main thread, not all...
r1201 self.threads.add(opening_post.get_thread())
neko259
Added tripcodes
r1293
neko259
Added ability to reset tripcode
r1296 def get_tripcode(self):
if self.tripcode:
return Tripcode(self.tripcode)
neko259
Reflinks to OPs are bold now. Refactored reflinks to build using the same code. Refactored autoescaping
r1309
def get_link_view(self):
"""
Gets view of a reflink to the post.
"""
result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
neko259
Favorite threads with new posts counter
r1323 self.id)
neko259
Reflinks to OPs are bold now. Refactored reflinks to build using the same code. Refactored autoescaping
r1309 if self.is_opening():
result = '<b>{}</b>'.format(result)
return result
neko259
Added ability to hide a post
r1366
def is_hidden(self) -> bool:
return self.hidden
def set_hidden(self, hidden):
self.hidden = hidden
neko259
Use signals for pre- and post- post actions
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)
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'])
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):
neko259
Use singleton parser
r1499 instance._text_rendered = get_parser().parse(instance.get_raw_text())