|
|
import os
|
|
|
from random import random
|
|
|
import re
|
|
|
import time
|
|
|
import math
|
|
|
|
|
|
from django.db import models
|
|
|
from django.http import Http404
|
|
|
from django.utils import timezone
|
|
|
from markupfield.fields import MarkupField
|
|
|
from threading import Thread
|
|
|
|
|
|
from neboard import settings
|
|
|
import thumbs
|
|
|
|
|
|
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 = '.'
|
|
|
|
|
|
REGEX_PRETTY = re.compile(r'^\d(0)+$')
|
|
|
REGEX_SAME = re.compile(r'^(.)\1+$')
|
|
|
|
|
|
|
|
|
class PostManager(models.Manager):
|
|
|
def create_post(self, title, text, image=None, parent_id=NO_PARENT,
|
|
|
ip=NO_IP, tags=None):
|
|
|
post = self.create(title=title,
|
|
|
text=text,
|
|
|
pub_time=timezone.now(),
|
|
|
parent=parent_id,
|
|
|
image=image,
|
|
|
poster_ip=ip,
|
|
|
poster_user_agent=UNKNOWN_UA,
|
|
|
last_edit_time=timezone.now())
|
|
|
|
|
|
if tags:
|
|
|
map(post.tags.add, tags)
|
|
|
|
|
|
if parent_id != NO_PARENT:
|
|
|
self._bump_thread(parent_id)
|
|
|
else:
|
|
|
self._delete_old_threads()
|
|
|
|
|
|
return post
|
|
|
|
|
|
def delete_post(self, post):
|
|
|
children = self.filter(parent=post.id)
|
|
|
for child in children:
|
|
|
self.delete_post(child)
|
|
|
post.delete()
|
|
|
|
|
|
def delete_posts_by_ip(self, ip):
|
|
|
posts = self.filter(poster_ip=ip)
|
|
|
for post in posts:
|
|
|
self.delete_post(post)
|
|
|
|
|
|
def get_threads(self, tag=None, page=ALL_PAGES,
|
|
|
order_by='-last_edit_time'):
|
|
|
if tag:
|
|
|
threads = self.filter(parent=NO_PARENT, tags=tag)
|
|
|
|
|
|
# TODO Throw error 404 if no threads for tag found?
|
|
|
else:
|
|
|
threads = self.filter(parent=NO_PARENT)
|
|
|
|
|
|
threads = threads.order_by(order_by)
|
|
|
|
|
|
if page != ALL_PAGES:
|
|
|
thread_count = len(threads)
|
|
|
|
|
|
if page < self.get_thread_page_count(tag=tag):
|
|
|
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, parent=NO_PARENT)
|
|
|
except Post.DoesNotExist:
|
|
|
raise Http404
|
|
|
|
|
|
if opening_post.parent == NO_PARENT:
|
|
|
replies = self.filter(parent=opening_post_id)
|
|
|
|
|
|
thread = [opening_post]
|
|
|
thread.extend(replies)
|
|
|
|
|
|
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(parent=NO_PARENT, tags=tag)
|
|
|
else:
|
|
|
threads = self.filter(parent=NO_PARENT)
|
|
|
|
|
|
return int(math.ceil(threads.count() / float(
|
|
|
settings.THREADS_PER_PAGE)))
|
|
|
|
|
|
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 = len(threads)
|
|
|
|
|
|
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:
|
|
|
self.delete_post(thread)
|
|
|
|
|
|
def _bump_thread(self, thread_id):
|
|
|
thread = self.get(id=thread_id)
|
|
|
|
|
|
if thread.can_bump():
|
|
|
thread.last_edit_time = timezone.now()
|
|
|
thread.save()
|
|
|
|
|
|
|
|
|
class TagManager(models.Manager):
|
|
|
def get_not_empty_tags(self):
|
|
|
all_tags = self.all().order_by('name')
|
|
|
tags = []
|
|
|
for tag in all_tags:
|
|
|
if not tag.is_empty():
|
|
|
tags.append(tag)
|
|
|
|
|
|
return tags
|
|
|
|
|
|
def get_popular_tags(self):
|
|
|
all_tags = self.get_not_empty_tags()
|
|
|
|
|
|
sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
|
|
|
reverse=True)
|
|
|
|
|
|
return sorted_tags[:settings.POPULAR_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)
|
|
|
# TODO Connect the tag to its posts to check the number of threads for
|
|
|
# the tag.
|
|
|
|
|
|
def __unicode__(self):
|
|
|
return self.name
|
|
|
|
|
|
def is_empty(self):
|
|
|
return self.get_post_count() == 0
|
|
|
|
|
|
def get_post_count(self):
|
|
|
posts_with_tag = Post.objects.get_threads(tag=self)
|
|
|
return posts_with_tag.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
|
|
|
|
|
|
|
|
|
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 = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
|
|
|
blank=True, sizes=(IMAGE_THUMB_SIZE,))
|
|
|
poster_ip = models.IPAddressField()
|
|
|
poster_user_agent = models.TextField()
|
|
|
parent = models.BigIntegerField()
|
|
|
tags = models.ManyToManyField(Tag)
|
|
|
last_edit_time = models.DateTimeField()
|
|
|
|
|
|
def __unicode__(self):
|
|
|
return '#' + str(self.id) + ' ' + self.title + ' (' + self.text.raw + \
|
|
|
')'
|
|
|
|
|
|
def _get_replies(self):
|
|
|
return Post.objects.filter(parent=self.id)
|
|
|
|
|
|
def get_reply_count(self):
|
|
|
return self._get_replies().count()
|
|
|
|
|
|
def get_images_count(self):
|
|
|
images_count = 1 if self.image else 0
|
|
|
for reply in self._get_replies():
|
|
|
if reply.image:
|
|
|
images_count += 1
|
|
|
|
|
|
return images_count
|
|
|
|
|
|
def get_gets_count(self):
|
|
|
gets_count = 1 if self.is_get() else 0
|
|
|
for reply in self._get_replies():
|
|
|
if reply.is_get():
|
|
|
gets_count += 1
|
|
|
|
|
|
return gets_count
|
|
|
|
|
|
def is_get(self):
|
|
|
"""If the post has pretty id (1, 1000, 77777), than it is called GET"""
|
|
|
|
|
|
first = self.id == 1
|
|
|
|
|
|
id_str = str(self.id)
|
|
|
pretty = REGEX_PRETTY.match(id_str)
|
|
|
same_digits = REGEX_SAME.match(id_str)
|
|
|
|
|
|
return first or pretty or same_digits
|
|
|
|
|
|
def can_bump(self):
|
|
|
"""Check if the thread can be bumped by replying"""
|
|
|
|
|
|
replies_count = len(Post.objects.get_thread(self.id))
|
|
|
|
|
|
return replies_count <= settings.MAX_POSTS_PER_THREAD
|
|
|
|
|
|
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._get_replies()[reply_count
|
|
|
- reply_count_to_show:]
|
|
|
|
|
|
return last_replies
|
|
|
|
|
|
|
|
|
class Admin(models.Model):
|
|
|
"""
|
|
|
Model for admin users
|
|
|
"""
|
|
|
name = models.CharField(max_length=100)
|
|
|
password = models.CharField(max_length=100)
|
|
|
|
|
|
def __unicode__(self):
|
|
|
return self.name + '/' + '*' * len(self.password)
|
|
|
|