|
|
import os
|
|
|
from random import random
|
|
|
import time
|
|
|
import math
|
|
|
from django.core.cache import cache
|
|
|
|
|
|
from django.db import models
|
|
|
from django.db.models import Count
|
|
|
from django.http import Http404
|
|
|
from django.utils import timezone
|
|
|
from markupfield.fields import MarkupField
|
|
|
from boards import settings as board_settings
|
|
|
|
|
|
from neboard import settings
|
|
|
import thumbs
|
|
|
|
|
|
import re
|
|
|
|
|
|
BAN_REASON_MAX_LENGTH = 200
|
|
|
|
|
|
BAN_REASON_AUTO = 'Auto'
|
|
|
|
|
|
IMAGE_THUMB_SIZE = (200, 150)
|
|
|
|
|
|
TITLE_MAX_LENGTH = 50
|
|
|
|
|
|
DEFAULT_MARKUP_TYPE = 'markdown'
|
|
|
|
|
|
NO_PARENT = -1
|
|
|
NO_IP = '0.0.0.0'
|
|
|
UNKNOWN_UA = ''
|
|
|
ALL_PAGES = -1
|
|
|
OPENING_POST_POPULARITY_WEIGHT = 2
|
|
|
IMAGES_DIRECTORY = 'images/'
|
|
|
FILE_EXTENSION_DELIMITER = '.'
|
|
|
|
|
|
RANK_ADMIN = 0
|
|
|
RANK_MODERATOR = 10
|
|
|
RANK_USER = 100
|
|
|
|
|
|
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):
|
|
|
posting_time = timezone.now()
|
|
|
|
|
|
post = self.create(title=title,
|
|
|
text=text,
|
|
|
pub_time=posting_time,
|
|
|
thread=thread,
|
|
|
image=image,
|
|
|
poster_ip=ip,
|
|
|
poster_user_agent=UNKNOWN_UA,
|
|
|
last_edit_time=posting_time,
|
|
|
bump_time=posting_time,
|
|
|
user=user)
|
|
|
|
|
|
if tags:
|
|
|
map(post.tags.add, tags)
|
|
|
for tag in tags:
|
|
|
tag.threads.add(post)
|
|
|
|
|
|
if thread:
|
|
|
thread.replies.add(post)
|
|
|
thread.bump()
|
|
|
thread.last_edit_time = posting_time
|
|
|
thread.save()
|
|
|
|
|
|
#cache_key = thread.get_cache_key()
|
|
|
#cache.delete(cache_key)
|
|
|
|
|
|
else:
|
|
|
self._delete_old_threads()
|
|
|
|
|
|
self.connect_replies(post)
|
|
|
|
|
|
return post
|
|
|
|
|
|
def delete_post(self, post):
|
|
|
if post.replies.count() > 0:
|
|
|
map(self.delete_post, post.replies.all())
|
|
|
|
|
|
# Update thread's last edit time (used as cache key)
|
|
|
thread = post.thread
|
|
|
if thread:
|
|
|
thread.last_edit_time = timezone.now()
|
|
|
thread.save()
|
|
|
|
|
|
#cache_key = thread.get_cache_key()
|
|
|
#cache.delete(cache_key)
|
|
|
|
|
|
post.delete()
|
|
|
|
|
|
def delete_posts_by_ip(self, ip):
|
|
|
posts = self.filter(poster_ip=ip)
|
|
|
map(self.delete_post, posts)
|
|
|
|
|
|
def get_threads(self, tag=None, page=ALL_PAGES,
|
|
|
order_by='-bump_time'):
|
|
|
if tag:
|
|
|
threads = tag.threads
|
|
|
|
|
|
if threads.count() == 0:
|
|
|
raise Http404
|
|
|
else:
|
|
|
threads = self.filter(thread=None)
|
|
|
|
|
|
threads = threads.order_by(order_by)
|
|
|
|
|
|
if page != ALL_PAGES:
|
|
|
thread_count = threads.count()
|
|
|
|
|
|
if page < self._get_page_count(thread_count):
|
|
|
start_thread = page * settings.THREADS_PER_PAGE
|
|
|
end_thread = min(start_thread + settings.THREADS_PER_PAGE,
|
|
|
thread_count)
|
|
|
threads = threads[start_thread:end_thread]
|
|
|
|
|
|
return threads
|
|
|
|
|
|
def get_thread(self, opening_post_id):
|
|
|
try:
|
|
|
opening_post = self.get(id=opening_post_id, thread=None)
|
|
|
except Post.DoesNotExist:
|
|
|
raise Http404
|
|
|
|
|
|
#cache_key = opening_post.get_cache_key()
|
|
|
#thread = cache.get(cache_key)
|
|
|
#if thread:
|
|
|
# return thread
|
|
|
|
|
|
if opening_post.replies:
|
|
|
thread = [opening_post]
|
|
|
thread.extend(opening_post.replies.all().order_by('pub_time'))
|
|
|
|
|
|
#cache.set(cache_key, thread, board_settings.CACHE_TIMEOUT)
|
|
|
|
|
|
return thread
|
|
|
|
|
|
def exists(self, post_id):
|
|
|
posts = self.filter(id=post_id)
|
|
|
|
|
|
return posts.count() > 0
|
|
|
|
|
|
def get_thread_page_count(self, tag=None):
|
|
|
if tag:
|
|
|
threads = self.filter(thread=None, tags=tag)
|
|
|
else:
|
|
|
threads = self.filter(thread=None)
|
|
|
|
|
|
return self._get_page_count(threads.count())
|
|
|
|
|
|
def _delete_old_threads(self):
|
|
|
"""
|
|
|
Preserves maximum thread count. If there are too many threads,
|
|
|
delete the old ones.
|
|
|
"""
|
|
|
|
|
|
# TODO Move old threads to the archive instead of deleting them.
|
|
|
# Maybe make some 'old' field in the model to indicate the thread
|
|
|
# must not be shown and be able for replying.
|
|
|
|
|
|
threads = self.get_threads()
|
|
|
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:]
|
|
|
|
|
|
map(self.delete_post, old_threads)
|
|
|
|
|
|
def connect_replies(self, post):
|
|
|
"""Connect replies to a post to show them as a refmap"""
|
|
|
|
|
|
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()
|
|
|
|
|
|
def _get_page_count(self, thread_count):
|
|
|
return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
|
|
|
|
|
|
|
|
|
class TagManager(models.Manager):
|
|
|
|
|
|
def get_not_empty_tags(self):
|
|
|
tags = self.annotate(Count('threads')) \
|
|
|
.filter(threads__count__gt=0).order_by('name')
|
|
|
|
|
|
return tags
|
|
|
|
|
|
|
|
|
class Tag(models.Model):
|
|
|
"""
|
|
|
A tag is a text node assigned to the post. The tag serves as a board
|
|
|
section. There can be multiple tags for each message
|
|
|
"""
|
|
|
|
|
|
objects = TagManager()
|
|
|
|
|
|
name = models.CharField(max_length=100)
|
|
|
threads = models.ManyToManyField('Post', null=True,
|
|
|
blank=True, related_name='tag+')
|
|
|
linked = models.ForeignKey('Tag', null=True, blank=True)
|
|
|
|
|
|
def __unicode__(self):
|
|
|
return self.name
|
|
|
|
|
|
def is_empty(self):
|
|
|
return self.get_post_count() == 0
|
|
|
|
|
|
def get_post_count(self):
|
|
|
return self.threads.count()
|
|
|
|
|
|
def get_popularity(self):
|
|
|
posts_with_tag = Post.objects.get_threads(tag=self)
|
|
|
reply_count = 0
|
|
|
for post in posts_with_tag:
|
|
|
reply_count += post.get_reply_count()
|
|
|
reply_count += OPENING_POST_POPULARITY_WEIGHT
|
|
|
|
|
|
return reply_count
|
|
|
|
|
|
def get_linked_tags(self):
|
|
|
tag_list = []
|
|
|
self.get_linked_tags_list(tag_list)
|
|
|
|
|
|
return tag_list
|
|
|
|
|
|
def get_linked_tags_list(self, tag_list=[]):
|
|
|
"""
|
|
|
Returns the list of tags linked to current. The list can be got
|
|
|
through returned value or tag_list parameter
|
|
|
"""
|
|
|
|
|
|
linked_tag = self.linked
|
|
|
|
|
|
if linked_tag and not (linked_tag in tag_list):
|
|
|
tag_list.append(linked_tag)
|
|
|
|
|
|
linked_tag.get_linked_tags_list(tag_list)
|
|
|
|
|
|
|
|
|
class Post(models.Model):
|
|
|
"""A post is a message."""
|
|
|
|
|
|
objects = PostManager()
|
|
|
|
|
|
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)
|
|
|
|
|
|
image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
|
|
|
blank=True, sizes=(IMAGE_THUMB_SIZE,),
|
|
|
width_field='image_width',
|
|
|
height_field='image_height')
|
|
|
|
|
|
poster_ip = models.GenericIPAddressField()
|
|
|
poster_user_agent = models.TextField()
|
|
|
|
|
|
thread = models.ForeignKey('Post', null=True, default=None)
|
|
|
tags = models.ManyToManyField(Tag)
|
|
|
last_edit_time = models.DateTimeField()
|
|
|
bump_time = models.DateTimeField()
|
|
|
user = models.ForeignKey('User', null=True, default=None)
|
|
|
|
|
|
replies = models.ManyToManyField('Post', symmetrical=False, null=True,
|
|
|
blank=True, related_name='re+')
|
|
|
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:
|
|
|
title = self.text.raw[:20]
|
|
|
|
|
|
return title
|
|
|
|
|
|
def get_reply_count(self):
|
|
|
return self.replies.count()
|
|
|
|
|
|
def get_images_count(self):
|
|
|
images_count = 1 if self.image else 0
|
|
|
images_count += self.replies.filter(image_width__gt=0).count()
|
|
|
|
|
|
return images_count
|
|
|
|
|
|
def can_bump(self):
|
|
|
"""Check if the thread can be bumped by replying"""
|
|
|
|
|
|
post_count = self.get_reply_count()
|
|
|
|
|
|
return post_count <= settings.MAX_POSTS_PER_THREAD
|
|
|
|
|
|
def bump(self):
|
|
|
"""Bump (move to up) thread"""
|
|
|
|
|
|
if self.can_bump():
|
|
|
self.bump_time = timezone.now()
|
|
|
|
|
|
def get_last_replies(self):
|
|
|
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)
|
|
|
last_replies = self.replies.all().order_by('pub_time')[
|
|
|
reply_count - reply_count_to_show:]
|
|
|
|
|
|
return last_replies
|
|
|
|
|
|
def get_tags(self):
|
|
|
"""Get a sorted tag list"""
|
|
|
|
|
|
return self.tags.order_by('name')
|
|
|
|
|
|
def get_cache_key(self):
|
|
|
return str(self.id) + str(self.last_edit_time.microsecond)
|
|
|
|
|
|
def get_sorted_referenced_posts(self):
|
|
|
return self.referenced_posts.order_by('id')
|
|
|
|
|
|
def is_referenced(self):
|
|
|
return self.referenced_posts.count() > 0
|
|
|
|
|
|
|
|
|
class User(models.Model):
|
|
|
|
|
|
user_id = models.CharField(max_length=50)
|
|
|
rank = models.IntegerField()
|
|
|
|
|
|
registration_time = models.DateTimeField()
|
|
|
|
|
|
fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
|
|
|
fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
|
|
|
blank=True)
|
|
|
|
|
|
def save_setting(self, name, value):
|
|
|
setting, created = Setting.objects.get_or_create(name=name, user=self)
|
|
|
setting.value = str(value)
|
|
|
setting.save()
|
|
|
|
|
|
return setting
|
|
|
|
|
|
def get_setting(self, name):
|
|
|
if Setting.objects.filter(name=name, user=self).exists():
|
|
|
setting = Setting.objects.get(name=name, user=self)
|
|
|
setting_value = setting.value
|
|
|
else:
|
|
|
setting_value = None
|
|
|
|
|
|
return setting_value
|
|
|
|
|
|
def is_moderator(self):
|
|
|
return RANK_MODERATOR >= self.rank
|
|
|
|
|
|
def get_sorted_fav_tags(self):
|
|
|
cache_key = self._get_tag_cache_key()
|
|
|
fav_tags = cache.get(cache_key)
|
|
|
if fav_tags:
|
|
|
return fav_tags
|
|
|
|
|
|
tags = self.fav_tags.annotate(Count('threads'))\
|
|
|
.filter(threads__count__gt=0).order_by('name')
|
|
|
|
|
|
if tags:
|
|
|
cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT)
|
|
|
|
|
|
return tags
|
|
|
|
|
|
def get_post_count(self):
|
|
|
return Post.objects.filter(user=self).count()
|
|
|
|
|
|
def __unicode__(self):
|
|
|
return self.user_id + '(' + str(self.rank) + ')'
|
|
|
|
|
|
def get_last_access_time(self):
|
|
|
posts = Post.objects.filter(user=self)
|
|
|
if posts.count() > 0:
|
|
|
return posts.latest('pub_time').pub_time
|
|
|
|
|
|
def add_tag(self, tag):
|
|
|
self.fav_tags.add(tag)
|
|
|
cache.delete(self._get_tag_cache_key())
|
|
|
|
|
|
def remove_tag(self, tag):
|
|
|
self.fav_tags.remove(tag)
|
|
|
cache.delete(self._get_tag_cache_key())
|
|
|
|
|
|
def _get_tag_cache_key(self):
|
|
|
return self.user_id + '_tags'
|
|
|
|
|
|
|
|
|
class Setting(models.Model):
|
|
|
|
|
|
name = models.CharField(max_length=50)
|
|
|
value = models.CharField(max_length=50)
|
|
|
user = models.ForeignKey(User)
|
|
|
|
|
|
|
|
|
class Ban(models.Model):
|
|
|
|
|
|
ip = models.GenericIPAddressField()
|
|
|
reason = models.CharField(default=BAN_REASON_AUTO,
|
|
|
max_length=BAN_REASON_MAX_LENGTH)
|
|
|
can_read = models.BooleanField(default=True)
|
|
|
|
|
|
def __unicode__(self):
|
|
|
return self.ip
|
|
|
|