##// END OF EJS Templates
Reflinks to OPs are bold now. Refactored reflinks to build using the same code. Refactored autoescaping
Reflinks to OPs are bold now. Refactored reflinks to build using the same code. Refactored autoescaping

File last commit:

r1269:819f972f default
r1309:a2eaff61 default
Show More
thread.py
230 lines | 7.0 KiB | text/x-python | PythonLexer
import logging
from adjacent import Client
from django.db.models import Count, Sum, QuerySet
from django.utils import timezone
from django.db import models
from boards import settings
import boards
from boards.utils import cached_result, datetime_to_epoch
from boards.models.post import Post
from boards.models.tag import Tag
__author__ = 'neko259'
logger = logging.getLogger(__name__)
WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
WS_NOTIFICATION_TYPE = 'notification_type'
WS_CHANNEL_THREAD = "thread:"
class ThreadManager(models.Manager):
def process_oldest_threads(self):
"""
Preserves maximum thread count. If there are too many threads,
archive or delete the old ones.
"""
threads = Thread.objects.filter(archived=False).order_by('-bump_time')
thread_count = threads.count()
max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
if thread_count > max_thread_count:
num_threads_to_delete = thread_count - max_thread_count
old_threads = threads[thread_count - num_threads_to_delete:]
for thread in old_threads:
if settings.get_bool('Storage', 'ArchiveThreads'):
self._archive_thread(thread)
else:
thread.delete()
logger.info('Processed %d old threads' % num_threads_to_delete)
def _archive_thread(self, thread):
thread.archived = True
thread.bumpable = False
thread.last_edit_time = timezone.now()
thread.update_posts_time()
thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
def get_thread_max_posts():
return settings.get_int('Messages', 'MaxPostsPerThread')
class Thread(models.Model):
objects = ThreadManager()
class Meta:
app_label = 'boards'
tags = models.ManyToManyField('Tag', related_name='thread_tags')
bump_time = models.DateTimeField(db_index=True)
last_edit_time = models.DateTimeField()
archived = models.BooleanField(default=False)
bumpable = models.BooleanField(default=True)
max_posts = models.IntegerField(default=get_thread_max_posts)
def get_tags(self) -> QuerySet:
"""
Gets a sorted tag list.
"""
return self.tags.order_by('name')
def bump(self):
"""
Bumps (moves to up) thread if possible.
"""
if self.can_bump():
self.bump_time = self.last_edit_time
self.update_bump_status()
logger.info('Bumped thread %d' % self.id)
def has_post_limit(self) -> bool:
return self.max_posts > 0
def update_bump_status(self, exclude_posts=None):
if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
self.bumpable = False
self.update_posts_time(exclude_posts=exclude_posts)
def _get_cache_key(self):
return [datetime_to_epoch(self.last_edit_time)]
@cached_result(key_method=_get_cache_key)
def get_reply_count(self) -> int:
return self.get_replies().count()
@cached_result(key_method=_get_cache_key)
def get_images_count(self) -> int:
return self.get_replies().annotate(images_count=Count(
'images')).aggregate(Sum('images_count'))['images_count__sum']
def can_bump(self) -> bool:
"""
Checks if the thread can be bumped by replying to it.
"""
return self.bumpable and not self.archived
def get_last_replies(self) -> QuerySet:
"""
Gets several last replies, not including opening post
"""
last_replies_count = settings.get_int('View', 'LastRepliesCount')
if last_replies_count > 0:
reply_count = self.get_reply_count()
if reply_count > 0:
reply_count_to_show = min(last_replies_count,
reply_count - 1)
replies = self.get_replies()
last_replies = replies[reply_count - reply_count_to_show:]
return last_replies
def get_skipped_replies_count(self) -> int:
"""
Gets number of posts between opening post and last replies.
"""
reply_count = self.get_reply_count()
last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
reply_count - 1)
return reply_count - last_replies_count - 1
def get_replies(self, view_fields_only=False) -> QuerySet:
"""
Gets sorted thread posts
"""
query = Post.objects.filter(threads__in=[self])
query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
if view_fields_only:
query = query.defer('poster_ip')
return query.all()
def get_top_level_replies(self) -> QuerySet:
return self.get_replies().exclude(refposts__threads__in=[self])
def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
"""
Gets replies that have at least one image attached
"""
return self.get_replies(view_fields_only).annotate(images_count=Count(
'images')).filter(images_count__gt=0)
def get_opening_post(self, only_id=False) -> Post:
"""
Gets the first post of the thread
"""
query = self.get_replies().order_by('pub_time')
if only_id:
query = query.only('id')
opening_post = query.first()
return opening_post
@cached_result()
def get_opening_post_id(self) -> int:
"""
Gets ID of the first thread post.
"""
return self.get_opening_post(only_id=True).id
def get_pub_time(self):
"""
Gets opening post's pub time because thread does not have its own one.
"""
return self.get_opening_post().pub_time
def __str__(self):
return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
def get_tag_url_list(self) -> list:
return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
def update_posts_time(self, exclude_posts=None):
last_edit_time = self.last_edit_time
for post in self.post_set.all():
if exclude_posts is None or post not in exclude_posts:
# Manual update is required because uids are generated on save
post.last_edit_time = last_edit_time
post.save(update_fields=['last_edit_time'])
post.get_threads().update(last_edit_time=last_edit_time)
def notify_clients(self):
if not settings.get_bool('External', 'WebsocketsEnabled'):
return
client = Client()
channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
client.publish(channel_name, {
WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
})
client.send()
def get_absolute_url(self):
return self.get_opening_post().get_absolute_url()
def get_required_tags(self):
return self.get_tags().filter(required=True)