post.py
438 lines
| 12.4 KiB
| text/x-python
|
PythonLexer
|
r586 | from datetime import datetime, timedelta, date | ||
|
r407 | from datetime import time as dtime | ||
|
r384 | import os | ||
from random import random | ||||
import time | ||||
import math | ||||
|
r386 | import re | ||
|
r527 | import hashlib | ||
|
r410 | from django.core.cache import cache | ||
|
r493 | from django.core.paginator import Paginator | ||
|
r589 | from django.core.urlresolvers import reverse | ||
|
r384 | |||
|
r566 | from django.db import models, transaction | ||
|
r384 | from django.http import Http404 | ||
from django.utils import timezone | ||||
from markupfield.fields import MarkupField | ||||
from neboard import settings | ||||
from boards import thumbs | ||||
|
r410 | APP_LABEL_BOARDS = 'boards' | ||
CACHE_KEY_PPD = 'ppd' | ||||
|
r589 | CACHE_KEY_POST_URL = 'post_url' | ||
|
r593 | CACHE_KEY_OPENING_POST = 'opening_post' | ||
|
r410 | |||
|
r408 | POSTS_PER_DAY_RANGE = range(7) | ||
|
r384 | BAN_REASON_AUTO = 'Auto' | ||
IMAGE_THUMB_SIZE = (200, 150) | ||||
|
r612 | TITLE_MAX_LENGTH = 200 | ||
|
r384 | |||
DEFAULT_MARKUP_TYPE = 'markdown' | ||||
NO_PARENT = -1 | ||||
NO_IP = '0.0.0.0' | ||||
UNKNOWN_UA = '' | ||||
ALL_PAGES = -1 | ||||
IMAGES_DIRECTORY = 'images/' | ||||
FILE_EXTENSION_DELIMITER = '.' | ||||
SETTING_MODERATE = "moderate" | ||||
REGEX_REPLY = re.compile('>>(\d+)') | ||||
class PostManager(models.Manager): | ||||
def create_post(self, title, text, image=None, thread=None, | ||||
ip=NO_IP, tags=None, user=None): | ||||
|
r418 | """ | ||
Create new post | ||||
""" | ||||
|
r384 | posting_time = timezone.now() | ||
|
r398 | if not thread: | ||
thread = Thread.objects.create(bump_time=posting_time, | ||||
last_edit_time=posting_time) | ||||
|
r615 | new_thread = True | ||
|
r398 | else: | ||
thread.bump() | ||||
thread.last_edit_time = posting_time | ||||
thread.save() | ||||
|
r615 | new_thread = False | ||
|
r384 | |||
post = self.create(title=title, | ||||
text=text, | ||||
pub_time=posting_time, | ||||
|
r398 | thread_new=thread, | ||
|
r384 | image=image, | ||
poster_ip=ip, | ||||
|
r484 | poster_user_agent=UNKNOWN_UA, # TODO Get UA at | ||
# last! | ||||
|
r384 | last_edit_time=posting_time, | ||
user=user) | ||||
|
r398 | thread.replies.add(post) | ||
|
r384 | if tags: | ||
linked_tags = [] | ||||
for tag in tags: | ||||
tag_linked_tags = tag.get_linked_tags() | ||||
if len(tag_linked_tags) > 0: | ||||
linked_tags.extend(tag_linked_tags) | ||||
tags.extend(linked_tags) | ||||
|
r398 | map(thread.add_tag, tags) | ||
|
r384 | |||
|
r615 | if new_thread: | ||
self._delete_old_threads() | ||||
|
r384 | self.connect_replies(post) | ||
return post | ||||
def delete_post(self, post): | ||||
|
r418 | """ | ||
|
r456 | Delete post and update or delete its thread | ||
|
r418 | """ | ||
|
r552 | |||
|
r456 | thread = post.thread_new | ||
|
r418 | |||
|
r552 | if post.is_opening(): | ||
thread.delete_with_posts() | ||||
|
r456 | else: | ||
thread.last_edit_time = timezone.now() | ||||
thread.save() | ||||
|
r384 | |||
|
r470 | post.delete() | ||
|
r384 | |||
def delete_posts_by_ip(self, ip): | ||||
|
r418 | """ | ||
Delete all posts of the author with same IP | ||||
""" | ||||
|
r384 | posts = self.filter(poster_ip=ip) | ||
map(self.delete_post, posts) | ||||
|
r398 | # TODO Move this method to thread manager | ||
|
r384 | def _delete_old_threads(self): | ||
""" | ||||
Preserves maximum thread count. If there are too many threads, | ||||
|
r484 | archive the old ones. | ||
|
r384 | """ | ||
|
r603 | threads = Thread.objects.filter(archived=False).order_by('-bump_time') | ||
|
r384 | 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:] | ||||
|
r484 | for thread in old_threads: | ||
thread.archived = True | ||||
|
r492 | thread.last_edit_time = timezone.now() | ||
|
r484 | thread.save() | ||
|
r384 | |||
def connect_replies(self, post): | ||||
|
r418 | """ | ||
Connect replies to a post to show them as a reflink map | ||||
""" | ||||
|
r384 | |||
for reply_number in re.finditer(REGEX_REPLY, post.text.raw): | ||||
post_id = reply_number.group(1) | ||||
ref_post = self.filter(id=post_id) | ||||
if ref_post.count() > 0: | ||||
referenced_post = ref_post[0] | ||||
referenced_post.referenced_posts.add(post) | ||||
referenced_post.last_edit_time = post.pub_time | ||||
referenced_post.save() | ||||
|
r535 | referenced_thread = referenced_post.thread_new | ||
referenced_thread.last_edit_time = post.pub_time | ||||
referenced_thread.save() | ||||
|
r407 | def get_posts_per_day(self): | ||
|
r418 | """ | ||
Get average count of posts per day for the last 7 days | ||||
""" | ||||
|
r407 | |||
|
r586 | today = date.today() | ||
|
r424 | ppd = cache.get(CACHE_KEY_PPD + str(today)) | ||
|
r410 | if ppd: | ||
return ppd | ||||
|
r408 | posts_per_days = [] | ||
for i in POSTS_PER_DAY_RANGE: | ||||
|
r413 | day_end = today - timedelta(i + 1) | ||
day_start = today - timedelta(i + 2) | ||||
|
r608 | day_time_start = timezone.make_aware(datetime.combine( | ||
day_start, dtime()), timezone.get_current_timezone()) | ||||
day_time_end = timezone.make_aware(datetime.combine( | ||||
day_end, dtime()), timezone.get_current_timezone()) | ||||
|
r408 | |||
|
r410 | posts_per_days.append(float(self.filter( | ||
pub_time__lte=day_time_end, | ||||
pub_time__gte=day_time_start).count())) | ||||
|
r408 | |||
|
r410 | ppd = (sum(posts_per_day for posts_per_day in posts_per_days) / | ||
len(posts_per_days)) | ||||
|
r569 | cache.set(CACHE_KEY_PPD + str(today), ppd) | ||
|
r410 | return ppd | ||
|
r407 | |||
|
r384 | |||
class Post(models.Model): | ||||
"""A post is a message.""" | ||||
objects = PostManager() | ||||
class Meta: | ||||
|
r410 | app_label = APP_LABEL_BOARDS | ||
|
r384 | |||
|
r418 | # TODO Save original file name to some field | ||
|
r384 | def _update_image_filename(self, filename): | ||
"""Get unique image filename""" | ||||
path = IMAGES_DIRECTORY | ||||
new_name = str(int(time.mktime(time.gmtime()))) | ||||
new_name += str(int(random() * 1000)) | ||||
new_name += FILE_EXTENSION_DELIMITER | ||||
new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] | ||||
return os.path.join(path, new_name) | ||||
title = models.CharField(max_length=TITLE_MAX_LENGTH) | ||||
pub_time = models.DateTimeField() | ||||
text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE, | ||||
escape_html=False) | ||||
image_width = models.IntegerField(default=0) | ||||
image_height = models.IntegerField(default=0) | ||||
|
r452 | image_pre_width = models.IntegerField(default=0) | ||
image_pre_height = models.IntegerField(default=0) | ||||
|
r384 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, | ||
blank=True, sizes=(IMAGE_THUMB_SIZE,), | ||||
width_field='image_width', | ||||
|
r452 | height_field='image_height', | ||
preview_width_field='image_pre_width', | ||||
preview_height_field='image_pre_height') | ||||
|
r527 | image_hash = models.CharField(max_length=36) | ||
|
r384 | |||
poster_ip = models.GenericIPAddressField() | ||||
poster_user_agent = models.TextField() | ||||
thread = models.ForeignKey('Post', null=True, default=None) | ||||
|
r398 | thread_new = models.ForeignKey('Thread', null=True, default=None) | ||
|
r384 | last_edit_time = models.DateTimeField() | ||
user = models.ForeignKey('User', null=True, default=None) | ||||
referenced_posts = models.ManyToManyField('Post', symmetrical=False, | ||||
null=True, | ||||
blank=True, related_name='rfp+') | ||||
def __unicode__(self): | ||||
return '#' + str(self.id) + ' ' + self.title + ' (' + \ | ||||
self.text.raw[:50] + ')' | ||||
def get_title(self): | ||||
title = self.title | ||||
if len(title) == 0: | ||||
|
r521 | title = self.text.rendered | ||
|
r384 | |||
return title | ||||
|
r398 | def get_sorted_referenced_posts(self): | ||
return self.referenced_posts.order_by('id') | ||||
def is_referenced(self): | ||||
return self.referenced_posts.all().exists() | ||||
|
r400 | def is_opening(self): | ||
|
r593 | return self.thread_new.get_opening_post() == self | ||
|
r400 | |||
|
r527 | def save(self, *args, **kwargs): | ||
""" | ||||
Save the model and compute the image hash | ||||
""" | ||||
if not self.pk and self.image: | ||||
md5 = hashlib.md5() | ||||
for chunk in self.image.chunks(): | ||||
md5.update(chunk) | ||||
self.image_hash = md5.hexdigest() | ||||
super(Post, self).save(*args, **kwargs) | ||||
|
r566 | @transaction.atomic | ||
def add_tag(self, tag): | ||||
edit_time = timezone.now() | ||||
thread = self.thread_new | ||||
thread.add_tag(tag) | ||||
self.last_edit_time = edit_time | ||||
self.save() | ||||
thread.last_edit_time = edit_time | ||||
thread.save() | ||||
@transaction.atomic | ||||
def remove_tag(self, tag): | ||||
edit_time = timezone.now() | ||||
thread = self.thread_new | ||||
|
r576 | thread.remove_tag(tag) | ||
|
r566 | self.last_edit_time = edit_time | ||
self.save() | ||||
thread.last_edit_time = edit_time | ||||
thread.save() | ||||
|
r614 | def get_url(self, opening_id=None): | ||
|
r589 | """ | ||
Get full url to this post | ||||
""" | ||||
cache_key = CACHE_KEY_POST_URL + str(self.id) | ||||
link = cache.get(cache_key) | ||||
if not link: | ||||
|
r614 | if not opening_id: | ||
opening_post = self.thread_new.get_opening_post() | ||||
opening_id = opening_post.id | ||||
if self.id != opening_id: | ||||
|
r608 | link = reverse('thread', kwargs={ | ||
|
r614 | 'post_id': opening_id}) + '#' + str(self.id) | ||
|
r589 | else: | ||
link = reverse('thread', kwargs={'post_id': self.id}) | ||||
cache.set(cache_key, link) | ||||
return link | ||||
|
r617 | def get_thread(self): | ||
# TODO Cache thread instead of getting it from the DB each time | ||||
return self.thread_new | ||||
|
r398 | |||
class Thread(models.Model): | ||||
class Meta: | ||||
|
r410 | app_label = APP_LABEL_BOARDS | ||
|
r398 | |||
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+') | ||||
|
r484 | archived = models.BooleanField(default=False) | ||
|
r398 | |||
def get_tags(self): | ||||
|
r418 | """ | ||
Get a sorted tag list | ||||
""" | ||||
|
r398 | |||
return self.tags.order_by('name') | ||||
def bump(self): | ||||
|
r418 | """ | ||
Bump (move to up) thread | ||||
""" | ||||
|
r398 | |||
if self.can_bump(): | ||||
self.bump_time = timezone.now() | ||||
|
r384 | def get_reply_count(self): | ||
return self.replies.count() | ||||
def get_images_count(self): | ||||
|
r398 | return self.replies.filter(image_width__gt=0).count() | ||
|
r384 | |||
def can_bump(self): | ||||
|
r418 | """ | ||
Check if the thread can be bumped by replying | ||||
""" | ||||
|
r384 | |||
|
r484 | if self.archived: | ||
return False | ||||
|
r384 | post_count = self.get_reply_count() | ||
|
r428 | return post_count < settings.MAX_POSTS_PER_THREAD | ||
|
r384 | |||
|
r398 | def delete_with_posts(self): | ||
|
r418 | """ | ||
Completely delete thread and all its posts | ||||
""" | ||||
|
r384 | |||
|
r398 | if self.replies.count() > 0: | ||
|
r470 | self.replies.all().delete() | ||
|
r398 | |||
self.delete() | ||||
|
r384 | |||
def get_last_replies(self): | ||||
|
r418 | """ | ||
Get last replies, not including opening post | ||||
""" | ||||
|
r398 | |||
|
r384 | 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, | ||||
|
r398 | reply_count - 1) | ||
|
r608 | last_replies = self.replies.all().order_by( | ||
'pub_time')[reply_count - reply_count_to_show:] | ||||
|
r384 | |||
return last_replies | ||||
|
r542 | def get_skipped_replies_count(self): | ||
last_replies = self.get_last_replies() | ||||
return self.get_reply_count() - len(last_replies) - 1 | ||||
|
r398 | def get_replies(self): | ||
|
r418 | """ | ||
Get sorted thread posts | ||||
""" | ||||
|
r384 | |||
|
r398 | return self.replies.all().order_by('pub_time') | ||
|
r384 | |||
|
r398 | def add_tag(self, tag): | ||
|
r418 | """ | ||
Connect thread to a tag and tag to a thread | ||||
""" | ||||
|
r384 | |||
|
r398 | self.tags.add(tag) | ||
tag.threads.add(self) | ||||
|
r576 | def remove_tag(self, tag): | ||
self.tags.remove(tag) | ||||
tag.threads.remove(self) | ||||
|
r400 | def get_opening_post(self): | ||
|
r418 | """ | ||
Get first post of the thread | ||||
""" | ||||
|
r595 | # cache_key = CACHE_KEY_OPENING_POST + str(self.id) | ||
# opening_post = cache.get(cache_key) | ||||
# if not opening_post: | ||||
opening_post = self.get_replies()[0] | ||||
# cache.set(cache_key, opening_post) | ||||
|
r593 | |||
return opening_post | ||||
|
r400 | |||
|
r398 | def __unicode__(self): | ||
|
r552 | return str(self.id) | ||
|
r402 | |||
def get_pub_time(self): | ||||
|
r418 | """ | ||
Thread does not have its own pub time, so we need to get it from | ||||
the opening post | ||||
""" | ||||
|
r411 | return self.get_opening_post().pub_time | ||