|
|
from datetime import datetime, timedelta, date
|
|
|
from datetime import time as dtime
|
|
|
import logging
|
|
|
import re
|
|
|
|
|
|
from django.core.cache import cache
|
|
|
from django.core.urlresolvers import reverse
|
|
|
from django.db import models, transaction
|
|
|
from django.template.loader import render_to_string
|
|
|
from django.utils import timezone
|
|
|
from markupfield.fields import MarkupField
|
|
|
|
|
|
from boards.models import PostImage
|
|
|
from boards.models.base import Viewable
|
|
|
from boards.models.thread import Thread
|
|
|
|
|
|
|
|
|
APP_LABEL_BOARDS = 'boards'
|
|
|
|
|
|
CACHE_KEY_PPD = 'ppd'
|
|
|
CACHE_KEY_POST_URL = 'post_url'
|
|
|
|
|
|
POSTS_PER_DAY_RANGE = 7
|
|
|
|
|
|
BAN_REASON_AUTO = 'Auto'
|
|
|
|
|
|
IMAGE_THUMB_SIZE = (200, 150)
|
|
|
|
|
|
TITLE_MAX_LENGTH = 200
|
|
|
|
|
|
DEFAULT_MARKUP_TYPE = 'bbcode'
|
|
|
|
|
|
# TODO This should be removed
|
|
|
NO_IP = '0.0.0.0'
|
|
|
|
|
|
# TODO Real user agent should be saved instead of this
|
|
|
UNKNOWN_UA = ''
|
|
|
|
|
|
REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
class PostManager(models.Manager):
|
|
|
def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
|
|
|
tags=None):
|
|
|
"""
|
|
|
Creates new post
|
|
|
"""
|
|
|
|
|
|
if not tags:
|
|
|
tags = []
|
|
|
|
|
|
posting_time = timezone.now()
|
|
|
if not thread:
|
|
|
thread = Thread.objects.create(bump_time=posting_time,
|
|
|
last_edit_time=posting_time)
|
|
|
new_thread = True
|
|
|
else:
|
|
|
thread.bump()
|
|
|
thread.last_edit_time = posting_time
|
|
|
thread.save()
|
|
|
new_thread = False
|
|
|
|
|
|
post = self.create(title=title,
|
|
|
text=text,
|
|
|
pub_time=posting_time,
|
|
|
thread_new=thread,
|
|
|
poster_ip=ip,
|
|
|
poster_user_agent=UNKNOWN_UA, # TODO Get UA at
|
|
|
# last!
|
|
|
last_edit_time=posting_time)
|
|
|
|
|
|
logger.info('Created post #%d with title %s' % (post.id,
|
|
|
post.title))
|
|
|
|
|
|
if image:
|
|
|
post_image = PostImage.objects.create(image=image)
|
|
|
post.images.add(post_image)
|
|
|
logger.info('Created image #%d for post #%d' % (post_image.id,
|
|
|
post.id))
|
|
|
|
|
|
thread.replies.add(post)
|
|
|
list(map(thread.add_tag, tags))
|
|
|
|
|
|
if new_thread:
|
|
|
Thread.objects.process_oldest_threads()
|
|
|
self.connect_replies(post)
|
|
|
|
|
|
return post
|
|
|
|
|
|
def delete_post(self, post):
|
|
|
"""
|
|
|
Deletes post and update or delete its thread
|
|
|
"""
|
|
|
|
|
|
post_id = post.id
|
|
|
|
|
|
thread = post.get_thread()
|
|
|
|
|
|
if post.is_opening():
|
|
|
thread.delete()
|
|
|
else:
|
|
|
thread.last_edit_time = timezone.now()
|
|
|
thread.save()
|
|
|
|
|
|
post.delete()
|
|
|
|
|
|
logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
|
|
|
|
|
|
def delete_posts_by_ip(self, ip):
|
|
|
"""
|
|
|
Deletes all posts of the author with same IP
|
|
|
"""
|
|
|
|
|
|
posts = self.filter(poster_ip=ip)
|
|
|
for post in posts:
|
|
|
self.delete_post(post)
|
|
|
|
|
|
def connect_replies(self, post):
|
|
|
"""
|
|
|
Connects replies to a post to show them as a reflink map
|
|
|
"""
|
|
|
|
|
|
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.build_refmap()
|
|
|
referenced_post.save(update_fields=['refmap', 'last_edit_time'])
|
|
|
|
|
|
referenced_thread = referenced_post.get_thread()
|
|
|
referenced_thread.last_edit_time = post.pub_time
|
|
|
referenced_thread.save(update_fields=['last_edit_time'])
|
|
|
|
|
|
def get_posts_per_day(self):
|
|
|
"""
|
|
|
Gets average count of posts per day for the last 7 days
|
|
|
"""
|
|
|
|
|
|
day_end = date.today()
|
|
|
day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
|
|
|
|
|
|
cache_key = CACHE_KEY_PPD + str(day_end)
|
|
|
ppd = cache.get(cache_key)
|
|
|
if ppd:
|
|
|
return ppd
|
|
|
|
|
|
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())
|
|
|
|
|
|
posts_per_period = float(self.filter(
|
|
|
pub_time__lte=day_time_end,
|
|
|
pub_time__gte=day_time_start).count())
|
|
|
|
|
|
ppd = posts_per_period / POSTS_PER_DAY_RANGE
|
|
|
|
|
|
cache.set(cache_key, ppd)
|
|
|
return ppd
|
|
|
|
|
|
|
|
|
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)
|
|
|
pub_time = models.DateTimeField()
|
|
|
text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
|
|
|
escape_html=False)
|
|
|
|
|
|
images = models.ManyToManyField(PostImage, null=True, blank=True,
|
|
|
related_name='ip+', db_index=True)
|
|
|
|
|
|
poster_ip = models.GenericIPAddressField()
|
|
|
poster_user_agent = models.TextField()
|
|
|
|
|
|
thread_new = models.ForeignKey('Thread', null=True, default=None,
|
|
|
db_index=True)
|
|
|
last_edit_time = models.DateTimeField()
|
|
|
|
|
|
referenced_posts = models.ManyToManyField('Post', symmetrical=False,
|
|
|
null=True,
|
|
|
blank=True, related_name='rfp+',
|
|
|
db_index=True)
|
|
|
refmap = models.TextField(null=True, blank=True)
|
|
|
|
|
|
def __unicode__(self):
|
|
|
return '#' + str(self.id) + ' ' + self.title + ' (' + \
|
|
|
self.text.raw[:50] + ')'
|
|
|
|
|
|
def get_title(self):
|
|
|
"""
|
|
|
Gets original post title or part of its text.
|
|
|
"""
|
|
|
|
|
|
title = self.title
|
|
|
if not title:
|
|
|
title = self.text.rendered
|
|
|
|
|
|
return title
|
|
|
|
|
|
def build_refmap(self):
|
|
|
"""
|
|
|
Builds a replies map string from replies list. This is a cache to stop
|
|
|
the server from recalculating the map on every post show.
|
|
|
"""
|
|
|
map_string = ''
|
|
|
|
|
|
first = True
|
|
|
for refpost in self.referenced_posts.all():
|
|
|
if not first:
|
|
|
map_string += ', '
|
|
|
map_string += '<a href="%s">>>%s</a>' % (refpost.get_url(),
|
|
|
refpost.id)
|
|
|
first = False
|
|
|
|
|
|
self.refmap = map_string
|
|
|
|
|
|
def get_sorted_referenced_posts(self):
|
|
|
return self.refmap
|
|
|
|
|
|
def is_referenced(self):
|
|
|
return len(self.refmap) > 0
|
|
|
|
|
|
def is_opening(self):
|
|
|
"""
|
|
|
Checks if this is an opening post or just a reply.
|
|
|
"""
|
|
|
|
|
|
return self.get_thread().get_opening_post_id() == self.id
|
|
|
|
|
|
@transaction.atomic
|
|
|
def add_tag(self, tag):
|
|
|
edit_time = timezone.now()
|
|
|
|
|
|
thread = self.get_thread()
|
|
|
thread.add_tag(tag)
|
|
|
self.last_edit_time = edit_time
|
|
|
self.save(update_fields=['last_edit_time'])
|
|
|
|
|
|
thread.last_edit_time = edit_time
|
|
|
thread.save(update_fields=['last_edit_time'])
|
|
|
|
|
|
@transaction.atomic
|
|
|
def remove_tag(self, tag):
|
|
|
edit_time = timezone.now()
|
|
|
|
|
|
thread = self.get_thread()
|
|
|
thread.remove_tag(tag)
|
|
|
self.last_edit_time = edit_time
|
|
|
self.save(update_fields=['last_edit_time'])
|
|
|
|
|
|
thread.last_edit_time = edit_time
|
|
|
thread.save(update_fields=['last_edit_time'])
|
|
|
|
|
|
def get_url(self, thread=None):
|
|
|
"""
|
|
|
Gets full url to the post.
|
|
|
"""
|
|
|
|
|
|
cache_key = CACHE_KEY_POST_URL + str(self.id)
|
|
|
link = cache.get(cache_key)
|
|
|
|
|
|
if not link:
|
|
|
if not thread:
|
|
|
thread = self.get_thread()
|
|
|
|
|
|
opening_id = thread.get_opening_post_id()
|
|
|
|
|
|
if self.id != opening_id:
|
|
|
link = reverse('thread', kwargs={
|
|
|
'post_id': opening_id}) + '#' + str(self.id)
|
|
|
else:
|
|
|
link = reverse('thread', kwargs={'post_id': self.id})
|
|
|
|
|
|
cache.set(cache_key, link)
|
|
|
|
|
|
return link
|
|
|
|
|
|
def get_thread(self):
|
|
|
"""
|
|
|
Gets post's thread.
|
|
|
"""
|
|
|
|
|
|
return self.thread_new
|
|
|
|
|
|
def get_referenced_posts(self):
|
|
|
return self.referenced_posts.only('id', 'thread_new')
|
|
|
|
|
|
def get_text(self):
|
|
|
return self.text
|
|
|
|
|
|
def get_view(self, moderator=False, need_open_link=False,
|
|
|
truncated=False, *args, **kwargs):
|
|
|
if 'is_opening' in kwargs:
|
|
|
is_opening = kwargs['is_opening']
|
|
|
else:
|
|
|
is_opening = self.is_opening()
|
|
|
|
|
|
if 'thread' in kwargs:
|
|
|
thread = kwargs['thread']
|
|
|
else:
|
|
|
thread = self.get_thread()
|
|
|
|
|
|
if 'can_bump' in kwargs:
|
|
|
can_bump = kwargs['can_bump']
|
|
|
else:
|
|
|
can_bump = thread.can_bump()
|
|
|
|
|
|
if is_opening:
|
|
|
opening_post_id = self.id
|
|
|
else:
|
|
|
opening_post_id = thread.get_opening_post_id()
|
|
|
|
|
|
return render_to_string('boards/post.html', {
|
|
|
'post': self,
|
|
|
'moderator': moderator,
|
|
|
'is_opening': is_opening,
|
|
|
'thread': thread,
|
|
|
'bumpable': can_bump,
|
|
|
'need_open_link': need_open_link,
|
|
|
'truncated': truncated,
|
|
|
'opening_post_id': opening_post_id,
|
|
|
})
|
|
|
|
|
|
def get_first_image(self):
|
|
|
return self.images.earliest('id')
|
|
|
|
|
|
def delete(self, using=None):
|
|
|
"""
|
|
|
Deletes all post images and the post itself.
|
|
|
"""
|
|
|
|
|
|
self.images.all().delete()
|
|
|
|
|
|
super(Post, self).delete(using)
|
|
|
|