##// END OF EJS Templates
New backend for fav threads. Now only last post ids are saved, no thread ids
New backend for fav threads. Now only last post ids are saved, no thread ids

File last commit:

r2044:227641ed default
r2044:227641ed default
Show More
thread.py
324 lines | 10.3 KiB | text/x-python | PythonLexer
import logging
from datetime import timedelta
from django.db import models, transaction
from django.db.models import Count, Sum, QuerySet, Q
from django.utils import timezone
import boards
from boards import settings
from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
from boards.models.attachment import FILE_TYPES_IMAGE
from boards.models.post import Post
from boards.models.tag import Tag, TagAlias
from boards.settings import SECTION_VIEW
from boards.utils import cached_result, datetime_to_epoch
FAV_THREAD_NO_UPDATES = -1
__author__ = 'neko259'
logger = logging.getLogger(__name__)
WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
WS_NOTIFICATION_TYPE = 'notification_type'
WS_CHANNEL_THREAD = "thread:"
STATUS_CHOICES = (
(STATUS_ACTIVE, STATUS_ACTIVE),
(STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
(STATUS_ARCHIVE, STATUS_ARCHIVE),
)
class ThreadManager(models.Manager):
def process_old_threads(self):
"""
Preserves maximum thread count. If there are too many threads,
archive or delete the old ones.
"""
old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
old_time = timezone.now() - timedelta(days=old_time_delta)
old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
for op in old_ops:
thread = op.get_thread()
if settings.get_bool('Storage', 'ArchiveThreads'):
self._archive_thread(thread)
else:
thread.delete()
logger.info('Processed old thread {}'.format(thread))
def _archive_thread(self, thread):
thread.status = STATUS_ARCHIVE
thread.last_edit_time = timezone.now()
thread.update_posts_time()
thread.save(update_fields=['last_edit_time', 'status'])
def get_new_posts(self, last_posts):
query = None
for post in last_posts:
if not post.get_thread().is_archived():
q = Q(id=post.thread_id) & Q(replies__id__gt=post.id)
if query is None:
query = q
else:
query = query | q
if query is not None:
return self.filter(query).annotate(
new_post_count=Count('replies'))
def get_new_post_count(self, last_posts):
new_posts = self.get_new_posts(last_posts)
return new_posts.aggregate(total_count=Count('replies'))\
['total_count'] if new_posts else 0
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()
max_posts = models.IntegerField(default=get_thread_max_posts)
status = models.CharField(max_length=50, default=STATUS_ACTIVE,
choices=STATUS_CHOICES, db_index=True)
monochrome = models.BooleanField(default=False)
stickerpack = models.BooleanField(default=False)
def get_tags(self) -> QuerySet:
"""
Gets a sorted tag list.
"""
return self.tags.filter(aliases__in=TagAlias.objects.filter_localized(parent__thread_tags=self)).order_by('aliases__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.status = STATUS_BUMPLIMIT
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().filter(
attachments__mimetype__in=FILE_TYPES_IMAGE)\
.annotate(images_count=Count(
'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
@cached_result(key_method=_get_cache_key)
def get_attachment_count(self) -> int:
return self.get_replies().annotate(attachment_count=Count('attachments'))\
.aggregate(Sum('attachment_count'))['attachment_count__sum'] or 0
def can_bump(self) -> bool:
"""
Checks if the thread can be bumped by replying to it.
"""
return self.get_status() == STATUS_ACTIVE
def get_last_replies(self) -> QuerySet:
"""
Gets several last replies, not including opening post
"""
last_replies_count = settings.get_int(SECTION_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(SECTION_VIEW, 'LastRepliesCount'),
reply_count - 1)
return reply_count - last_replies_count - 1
# TODO Remove argument, it is not used
def get_replies(self, view_fields_only=True) -> QuerySet:
"""
Gets sorted thread posts
"""
query = self.replies.order_by('pub_time').prefetch_related(
'attachments')
return query
def get_viewable_replies(self) -> QuerySet:
"""
Gets replies with only fields that are used for viewing.
"""
return self.get_replies().defer('text', 'last_edit_time')
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).filter(
attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
'attachments')).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().filter(opening=True)
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())
def get_tag_url_list(self) -> list:
return boards.models.Tag.objects.get_tag_url_list(self.get_tags().all())
def update_posts_time(self, exclude_posts=None):
last_edit_time = self.last_edit_time
for post in self.replies.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'])
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)
def get_sections_str(self):
return Tag.objects.get_tag_url_list(self.get_required_tags())
def get_replies_newer(self, post_id):
return self.get_replies().filter(id__gt=post_id)
def is_archived(self):
return self.get_status() == STATUS_ARCHIVE
def is_bumplimit(self):
return self.get_status() == STATUS_BUMPLIMIT
def get_status(self):
return self.status
def is_monochrome(self):
return self.monochrome
def is_stickerpack(self):
return self.stickerpack
# If tags have parent, add them to the tag list
@transaction.atomic
def refresh_tags(self):
for tag in self.get_tags().all():
parents = tag.get_all_parents()
if len(parents) > 0:
self.tags.add(*parents)
def get_reply_tree(self):
replies = self.get_replies().prefetch_related('refposts')
tree = []
for reply in replies:
parents = reply.refposts.all()
found_parent = False
searching_for_index = False
if len(parents) > 0:
index = 0
parent_depth = 0
indexes_to_insert = []
for depth, element in tree:
index += 1
# If this element is next after parent on the same level,
# insert child before it
if searching_for_index and depth <= parent_depth:
indexes_to_insert.append((index - 1, parent_depth))
searching_for_index = False
if element in parents:
found_parent = True
searching_for_index = True
parent_depth = depth
if not found_parent:
tree.append((0, reply))
else:
if searching_for_index:
tree.append((parent_depth + 1, reply))
offset = 0
for last_index, parent_depth in indexes_to_insert:
tree.insert(last_index + offset, (parent_depth + 1, reply))
offset += 1
return tree