import logging from django.db.models import Count, Sum from django.utils import timezone from django.core.cache import cache from django.db import models from boards import settings __author__ = 'neko259' logger = logging.getLogger(__name__) CACHE_KEY_OPENING_POST = 'opening_post_id' 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() if thread_count > settings.MAX_THREAD_COUNT: num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT old_threads = threads[thread_count - num_threads_to_delete:] for thread in old_threads: if settings.ARCHIVE_THREADS: 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.save(update_fields=['archived', 'last_edit_time', 'bumpable']) class Thread(models.Model): objects = ThreadManager() class Meta: app_label = 'boards' tags = models.ManyToManyField('Tag') bump_time = models.DateTimeField() last_edit_time = models.DateTimeField() replies = models.ManyToManyField('Post', symmetrical=False, null=True, blank=True, related_name='tre+') archived = models.BooleanField(default=False) bumpable = models.BooleanField(default=True) def get_tags(self): """ 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 = timezone.now() if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD: self.bumpable = False logger.info('Bumped thread %d' % self.id) def get_reply_count(self): return self.replies.count() def get_images_count(self): return self.replies.annotate(images_count=Count( 'images')).aggregate(Sum('images_count'))['images_count__sum'] def can_bump(self): """ Checks if the thread can be bumped by replying to it. """ return self.bumpable def get_last_replies(self): """ Gets several last replies, not including opening post """ if settings.LAST_REPLIES_COUNT > 0: reply_count = self.get_reply_count() if reply_count > 0: reply_count_to_show = min(settings.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): """ Gets number of posts between opening post and last replies. """ reply_count = self.get_reply_count() last_replies_count = min(settings.LAST_REPLIES_COUNT, reply_count - 1) return reply_count - last_replies_count - 1 def get_replies(self, view_fields_only=False): """ Gets sorted thread posts """ query = self.replies.order_by('pub_time').prefetch_related('images') if view_fields_only: query = query.defer('poster_user_agent') return query.all() def get_replies_with_images(self, view_fields_only=False): """ 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 add_tag(self, tag): """ Connects thread to a tag and tag to a thread """ self.tags.add(tag) def get_opening_post(self, only_id=False): """ Gets the first post of the thread """ query = self.replies.order_by('pub_time') if only_id: query = query.only('id') opening_post = query.first() return opening_post def get_opening_post_id(self): """ Gets ID of the first thread post. """ cache_key = CACHE_KEY_OPENING_POST + str(self.id) opening_post_id = cache.get(cache_key) if not opening_post_id: opening_post_id = self.get_opening_post(only_id=True).id cache.set(cache_key, opening_post_id) return opening_post_id def __unicode__(self): return str(self.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 delete(self, using=None): """ Deletes thread with all replies. """ for reply in self.replies.all(): reply.delete() super(Thread, self).delete(using) def __str__(self): return 'T#{}/{}'.format(self.id, self.get_opening_post_id())