models.py
332 lines
| 9.5 KiB
| text/x-python
|
PythonLexer
/ boards / models.py
|
r29 | import os | ||
|
r50 | from random import random | ||
|
r31 | import re | ||
|
r29 | import time | ||
|
r46 | import math | ||
|
r71 | |||
from django.db import models | ||||
|
r169 | from django.db.models import Count | ||
|
r71 | from django.http import Http404 | ||
from django.utils import timezone | ||||
from markupfield.fields import MarkupField | ||||
|
r4 | |||
|
r22 | from neboard import settings | ||
|
r71 | import thumbs | ||
|
r22 | |||
|
r79 | IMAGE_THUMB_SIZE = (200, 150) | ||
TITLE_MAX_LENGTH = 50 | ||||
DEFAULT_MARKUP_TYPE = 'markdown' | ||||
|
r22 | |||
|
r4 | NO_PARENT = -1 | ||
NO_IP = '0.0.0.0' | ||||
UNKNOWN_UA = '' | ||||
|
r71 | ALL_PAGES = -1 | ||
|
r85 | OPENING_POST_POPULARITY_WEIGHT = 2 | ||
|
r71 | IMAGES_DIRECTORY = 'images/' | ||
FILE_EXTENSION_DELIMITER = '.' | ||||
|
r112 | RANK_ADMIN = 0 | ||
RANK_MODERATOR = 10 | ||||
RANK_USER = 100 | ||||
|
r0 | |||
|
r143 | |||
|
r2 | class PostManager(models.Manager): | ||
|
r185 | |||
|
r174 | def create_post(self, title, text, image=None, thread=None, | ||
|
r113 | ip=NO_IP, tags=None, user=None): | ||
|
r22 | post = self.create(title=title, | ||
text=text, | ||||
pub_time=timezone.now(), | ||||
|
r174 | thread=thread, | ||
|
r22 | image=image, | ||
poster_ip=ip, | ||||
|
r25 | poster_user_agent=UNKNOWN_UA, | ||
|
r113 | last_edit_time=timezone.now(), | ||
|
r186 | bump_time=timezone.now(), | ||
|
r113 | user=user) | ||
|
r0 | |||
|
r24 | if tags: | ||
|
r79 | map(post.tags.add, tags) | ||
|
r184 | for tag in tags: | ||
tag.threads.add(post) | ||||
|
r24 | |||
|
r174 | if thread: | ||
|
r176 | thread.replies.add(post) | ||
|
r174 | thread.bump() | ||
|
r186 | thread.last_edit_time = timezone.now() | ||
thread.save() | ||||
|
r28 | else: | ||
self._delete_old_threads() | ||||
|
r25 | |||
|
r1 | return post | ||
|
r0 | |||
def delete_post(self, post): | ||||
|
r169 | if post.replies.count() > 0: | ||
|
r174 | map(self.delete_post, post.replies.all()) | ||
|
r2 | post.delete() | ||
|
r0 | def delete_posts_by_ip(self, ip): | ||
|
r22 | posts = self.filter(poster_ip=ip) | ||
|
r147 | map(self.delete_post, posts) | ||
|
r0 | |||
|
r87 | def get_threads(self, tag=None, page=ALL_PAGES, | ||
|
r186 | order_by='-bump_time'): | ||
|
r27 | if tag: | ||
|
r183 | threads = tag.threads | ||
|
r174 | |||
# TODO This needs to be uncommented when 'all tags' view won't | ||||
# use this method to get threads for tag | ||||
|
r87 | |||
|
r174 | # if threads.count() == 0: | ||
# raise Http404 | ||||
|
r27 | else: | ||
|
r174 | threads = self.filter(thread=None) | ||
|
r79 | |||
|
r87 | threads = threads.order_by(order_by) | ||
|
r5 | |||
|
r71 | if page != ALL_PAGES: | ||
|
r163 | thread_count = threads.count() | ||
|
r46 | |||
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] | ||||
|
r5 | return threads | ||
def get_thread(self, opening_post_id): | ||||
|
r79 | try: | ||
|
r174 | opening_post = self.get(id=opening_post_id, thread=None) | ||
|
r79 | except Post.DoesNotExist: | ||
|
r71 | raise Http404 | ||
|
r169 | if opening_post.replies: | ||
|
r33 | thread = [opening_post] | ||
|
r169 | thread.extend(opening_post.replies.all()) | ||
|
r17 | |||
|
r33 | return thread | ||
|
r5 | |||
|
r8 | def exists(self, post_id): | ||
|
r22 | posts = self.filter(id=post_id) | ||
|
r8 | |||
|
r58 | return posts.count() > 0 | ||
|
r8 | |||
|
r46 | def get_thread_page_count(self, tag=None): | ||
if tag: | ||||
|
r174 | threads = self.filter(thread=None, tags=tag) | ||
|
r46 | else: | ||
|
r174 | threads = self.filter(thread=None) | ||
|
r46 | |||
|
r58 | return int(math.ceil(threads.count() / float( | ||
settings.THREADS_PER_PAGE))) | ||||
|
r46 | |||
|
r28 | 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 | ||||
|
r58 | old_threads = threads[thread_count - num_threads_to_delete:] | ||
|
r28 | |||
|
r147 | map(self.delete_post, old_threads) | ||
|
r28 | |||
|
r5 | |||
|
r33 | class TagManager(models.Manager): | ||
|
r176 | |||
|
r33 | def get_not_empty_tags(self): | ||
|
r182 | tags = self.annotate(Count('threads')) \ | ||
.filter(threads__count__gt=0).order_by('name') | ||||
|
r33 | |||
return tags | ||||
|
r4 | 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 | ||||
""" | ||||
|
r33 | objects = TagManager() | ||
|
r22 | name = models.CharField(max_length=100) | ||
|
r182 | threads = models.ManyToManyField('Post', null=True, | ||
blank=True, related_name='tag+') | ||||
|
r4 | |||
|
r31 | def __unicode__(self): | ||
return self.name | ||||
|
r32 | def is_empty(self): | ||
|
r33 | return self.get_post_count() == 0 | ||
def get_post_count(self): | ||||
|
r182 | return self.threads.count() | ||
|
r32 | |||
|
r57 | def get_popularity(self): | ||
|
r178 | posts_with_tag = Post.objects.get_threads(tag=self) | ||
|
r57 | reply_count = 0 | ||
for post in posts_with_tag: | ||||
reply_count += post.get_reply_count() | ||||
|
r85 | reply_count += OPENING_POST_POPULARITY_WEIGHT | ||
|
r57 | |||
return reply_count | ||||
|
r8 | |||
|
r2 | class Post(models.Model): | ||
|
r4 | """A post is a message.""" | ||
|
r0 | |||
|
r2 | objects = PostManager() | ||
|
r0 | |||
|
r50 | def _update_image_filename(self, filename): | ||
"""Get unique image filename""" | ||||
|
r71 | path = IMAGES_DIRECTORY | ||
|
r50 | new_name = str(int(time.mktime(time.gmtime()))) | ||
new_name += str(int(random() * 1000)) | ||||
|
r71 | new_name += FILE_EXTENSION_DELIMITER | ||
new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] | ||||
|
r50 | |||
return os.path.join(path, new_name) | ||||
|
r79 | title = models.CharField(max_length=TITLE_MAX_LENGTH) | ||
|
r2 | pub_time = models.DateTimeField() | ||
|
r79 | text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE, | ||
|
r90 | escape_html=False) | ||
|
r127 | |||
|
r129 | image_width = models.IntegerField(default=0) | ||
image_height = models.IntegerField(default=0) | ||||
|
r127 | |||
|
r50 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, | ||
|
r127 | blank=True, sizes=(IMAGE_THUMB_SIZE,), | ||
width_field='image_width', | ||||
height_field='image_height') | ||||
|
r116 | poster_ip = models.GenericIPAddressField() | ||
|
r2 | poster_user_agent = models.TextField() | ||
|
r169 | |||
|
r176 | # TODO Remove this field after everything has been updated to 'thread' | ||
|
r174 | parent = models.BigIntegerField(default=NO_PARENT) | ||
|
r169 | |||
|
r174 | thread = models.ForeignKey('Post', null=True, default=None) | ||
|
r7 | tags = models.ManyToManyField(Tag) | ||
|
r25 | last_edit_time = models.DateTimeField() | ||
|
r186 | bump_time = models.DateTimeField() | ||
|
r140 | user = models.ForeignKey('User', null=True, default=None) | ||
|
r0 | |||
|
r169 | replies = models.ManyToManyField('Post', symmetrical=False, null=True, | ||
blank=True, related_name='re+') | ||||
|
r0 | def __unicode__(self): | ||
|
r105 | return '#' + str(self.id) + ' ' + self.title + ' (' + \ | ||
|
r127 | self.text.raw[:50] + ')' | ||
|
r9 | |||
|
r147 | def get_title(self): | ||
title = self.title | ||||
if len(title) == 0: | ||||
title = self.text.raw[:20] | ||||
return title | ||||
|
r30 | def get_reply_count(self): | ||
|
r169 | return self.replies.count() | ||
|
r30 | |||
def get_images_count(self): | ||||
images_count = 1 if self.image else 0 | ||||
|
r177 | images_count += self.replies.filter(image_width__gt=0).count() | ||
|
r30 | |||
return images_count | ||||
|
r46 | def can_bump(self): | ||
"""Check if the thread can be bumped by replying""" | ||||
|
r169 | post_count = self.get_reply_count() + 1 | ||
|
r46 | |||
|
r169 | return post_count <= settings.MAX_POSTS_PER_THREAD | ||
|
r31 | |||
|
r174 | def bump(self): | ||
|
r176 | """Bump (move to up) thread""" | ||
|
r174 | if self.can_bump(): | ||
|
r186 | self.bump_time = timezone.now() | ||
|
r174 | |||
|
r175 | def get_last_replies(self): | ||
|
r59 | 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) | ||||
|
r169 | last_replies = self.replies.all()[reply_count - | ||
reply_count_to_show:] | ||||
|
r59 | |||
return last_replies | ||||
|
r9 | |||
|
r121 | class User(models.Model): | ||
user_id = models.CharField(max_length=50) | ||||
rank = models.IntegerField() | ||||
registration_time = models.DateTimeField() | ||||
last_access_time = models.DateTimeField() | ||||
|
r144 | fav_tags = models.ManyToManyField(Tag, null=True, blank=True) | ||
fav_threads = models.ManyToManyField(Post, related_name='+', null=True, | ||||
blank=True) | ||||
|
r121 | |||
def save_setting(self, name, value): | ||||
setting, created = Setting.objects.get_or_create(name=name, user=self) | ||||
setting.value = value | ||||
setting.save() | ||||
return setting | ||||
def get_setting(self, name): | ||||
|
r161 | if Setting.objects.filter(name=name, user=self).exists(): | ||
setting = Setting.objects.get(name=name, user=self) | ||||
|
r121 | setting_value = setting.value | ||
else: | ||||
setting_value = None | ||||
return setting_value | ||||
def is_moderator(self): | ||||
return RANK_MODERATOR >= self.rank | ||||
|
r9 | |||
|
r149 | def get_sorted_fav_tags(self): | ||
|
r182 | tags = self.fav_tags.annotate(Count('threads'))\ | ||
.filter(threads__count__gt=0).order_by('name') | ||||
return tags | ||||
|
r149 | |||
|
r185 | def get_post_count(self): | ||
return Post.objects.filter(user=self).count() | ||||
|
r9 | def __unicode__(self): | ||
|
r156 | return self.user_id + '(' + str(self.rank) + ')' | ||
|
r110 | |||
|
r140 | |||
|
r112 | class Setting(models.Model): | ||
|
r116 | |||
|
r112 | name = models.CharField(max_length=50) | ||
value = models.CharField(max_length=50) | ||||
user = models.ForeignKey(User) | ||||
|
r116 | |||
|
r144 | |||
|
r116 | class Ban(models.Model): | ||
|
r176 | |||
|
r116 | ip = models.GenericIPAddressField() | ||
def __unicode__(self): | ||||
return self.ip | ||||