models.py
439 lines
| 12.5 KiB
| text/x-python
|
PythonLexer
/ boards / models.py
neko259
|
r29 | import os | ||
neko259
|
r50 | from random import random | ||
neko259
|
r29 | import time | ||
neko259
|
r46 | import math | ||
neko259
|
r323 | from django.core.cache import cache | ||
neko259
|
r71 | |||
from django.db import models | ||||
neko259
|
r169 | from django.db.models import Count | ||
neko259
|
r71 | from django.http import Http404 | ||
from django.utils import timezone | ||||
from markupfield.fields import MarkupField | ||||
neko259
|
r323 | from boards import settings as board_settings | ||
neko259
|
r4 | |||
neko259
|
r22 | from neboard import settings | ||
neko259
|
r71 | import thumbs | ||
neko259
|
r22 | |||
neko259
|
r291 | import re | ||
neko259
|
r340 | BAN_REASON_MAX_LENGTH = 200 | ||
BAN_REASON_AUTO = 'Auto' | ||||
neko259
|
r79 | IMAGE_THUMB_SIZE = (200, 150) | ||
TITLE_MAX_LENGTH = 50 | ||||
DEFAULT_MARKUP_TYPE = 'markdown' | ||||
neko259
|
r22 | |||
neko259
|
r4 | NO_PARENT = -1 | ||
NO_IP = '0.0.0.0' | ||||
UNKNOWN_UA = '' | ||||
neko259
|
r71 | ALL_PAGES = -1 | ||
neko259
|
r85 | OPENING_POST_POPULARITY_WEIGHT = 2 | ||
neko259
|
r71 | IMAGES_DIRECTORY = 'images/' | ||
FILE_EXTENSION_DELIMITER = '.' | ||||
neko259
|
r112 | RANK_ADMIN = 0 | ||
RANK_MODERATOR = 10 | ||||
neko259
|
r291 | RANK_USER = 100 | ||
neko259
|
r205 | |||
SETTING_MODERATE = "moderate" | ||||
neko259
|
r0 | |||
neko259
|
r291 | REGEX_REPLY = re.compile('>>(\d+)') | ||
neko259
|
r143 | |||
neko259
|
r2 | class PostManager(models.Manager): | ||
neko259
|
r185 | |||
neko259
|
r174 | def create_post(self, title, text, image=None, thread=None, | ||
neko259
|
r113 | ip=NO_IP, tags=None, user=None): | ||
neko259
|
r370 | posting_time = timezone.now() | ||
neko259
|
r22 | post = self.create(title=title, | ||
text=text, | ||||
pub_time=timezone.now(), | ||||
neko259
|
r174 | thread=thread, | ||
neko259
|
r22 | image=image, | ||
poster_ip=ip, | ||||
neko259
|
r25 | poster_user_agent=UNKNOWN_UA, | ||
neko259
|
r370 | last_edit_time=posting_time, | ||
bump_time=posting_time, | ||||
neko259
|
r113 | user=user) | ||
neko259
|
r0 | |||
neko259
|
r24 | if tags: | ||
neko259
|
r79 | map(post.tags.add, tags) | ||
neko259
|
r184 | for tag in tags: | ||
tag.threads.add(post) | ||||
neko259
|
r24 | |||
neko259
|
r174 | if thread: | ||
neko259
|
r176 | thread.replies.add(post) | ||
neko259
|
r174 | thread.bump() | ||
neko259
|
r370 | thread.last_edit_time = posting_time | ||
neko259
|
r186 | thread.save() | ||
neko259
|
r324 | |||
neko259
|
r328 | #cache_key = thread.get_cache_key() | ||
#cache.delete(cache_key) | ||||
neko259
|
r324 | |||
neko259
|
r28 | else: | ||
self._delete_old_threads() | ||||
neko259
|
r25 | |||
neko259
|
r318 | self.connect_replies(post) | ||
neko259
|
r291 | |||
neko259
|
r1 | return post | ||
neko259
|
r0 | |||
def delete_post(self, post): | ||||
neko259
|
r169 | if post.replies.count() > 0: | ||
neko259
|
r174 | map(self.delete_post, post.replies.all()) | ||
Pavel Ryapolov
|
r206 | |||
# Update thread's last edit time (used as cache key) | ||||
thread = post.thread | ||||
if thread: | ||||
thread.last_edit_time = timezone.now() | ||||
thread.save() | ||||
neko259
|
r328 | #cache_key = thread.get_cache_key() | ||
#cache.delete(cache_key) | ||||
neko259
|
r324 | |||
neko259
|
r2 | post.delete() | ||
neko259
|
r0 | def delete_posts_by_ip(self, ip): | ||
neko259
|
r22 | posts = self.filter(poster_ip=ip) | ||
neko259
|
r147 | map(self.delete_post, posts) | ||
neko259
|
r0 | |||
neko259
|
r87 | def get_threads(self, tag=None, page=ALL_PAGES, | ||
neko259
|
r186 | order_by='-bump_time'): | ||
neko259
|
r27 | if tag: | ||
neko259
|
r183 | threads = tag.threads | ||
neko259
|
r174 | |||
neko259
|
r202 | if threads.count() == 0: | ||
raise Http404 | ||||
neko259
|
r27 | else: | ||
neko259
|
r174 | threads = self.filter(thread=None) | ||
neko259
|
r79 | |||
neko259
|
r87 | threads = threads.order_by(order_by) | ||
neko259
|
r5 | |||
neko259
|
r71 | if page != ALL_PAGES: | ||
neko259
|
r163 | thread_count = threads.count() | ||
neko259
|
r46 | |||
neko259
|
r318 | if page < self._get_page_count(thread_count): | ||
neko259
|
r46 | start_thread = page * settings.THREADS_PER_PAGE | ||
end_thread = min(start_thread + settings.THREADS_PER_PAGE, | ||||
thread_count) | ||||
threads = threads[start_thread:end_thread] | ||||
neko259
|
r5 | return threads | ||
def get_thread(self, opening_post_id): | ||||
neko259
|
r79 | try: | ||
neko259
|
r174 | opening_post = self.get(id=opening_post_id, thread=None) | ||
neko259
|
r79 | except Post.DoesNotExist: | ||
neko259
|
r71 | raise Http404 | ||
neko259
|
r328 | #cache_key = opening_post.get_cache_key() | ||
#thread = cache.get(cache_key) | ||||
#if thread: | ||||
# return thread | ||||
neko259
|
r324 | |||
neko259
|
r169 | if opening_post.replies: | ||
neko259
|
r33 | thread = [opening_post] | ||
neko259
|
r215 | thread.extend(opening_post.replies.all().order_by('pub_time')) | ||
neko259
|
r17 | |||
neko259
|
r328 | #cache.set(cache_key, thread, board_settings.CACHE_TIMEOUT) | ||
neko259
|
r324 | |||
neko259
|
r33 | return thread | ||
neko259
|
r5 | |||
neko259
|
r8 | def exists(self, post_id): | ||
neko259
|
r22 | posts = self.filter(id=post_id) | ||
neko259
|
r8 | |||
neko259
|
r58 | return posts.count() > 0 | ||
neko259
|
r8 | |||
neko259
|
r46 | def get_thread_page_count(self, tag=None): | ||
if tag: | ||||
neko259
|
r174 | threads = self.filter(thread=None, tags=tag) | ||
neko259
|
r46 | else: | ||
neko259
|
r174 | threads = self.filter(thread=None) | ||
neko259
|
r46 | |||
neko259
|
r318 | return self._get_page_count(threads.count()) | ||
neko259
|
r46 | |||
neko259
|
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() | ||||
neko259
|
r251 | thread_count = threads.count() | ||
neko259
|
r28 | |||
if thread_count > settings.MAX_THREAD_COUNT: | ||||
num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT | ||||
neko259
|
r58 | old_threads = threads[thread_count - num_threads_to_delete:] | ||
neko259
|
r28 | |||
neko259
|
r147 | map(self.delete_post, old_threads) | ||
neko259
|
r28 | |||
neko259
|
r291 | def connect_replies(self, post): | ||
neko259
|
r318 | """Connect replies to a post to show them as a refmap""" | ||
neko259
|
r291 | |||
neko259
|
r318 | for reply_number in re.finditer(REGEX_REPLY, post.text.raw): | ||
id = reply_number.group(1) | ||||
ref_post = self.filter(id=id) | ||||
if ref_post.count() > 0: | ||||
neko259
|
r364 | referenced_post = ref_post[0] | ||
referenced_post.referenced_posts.add(post) | ||||
referenced_post.last_edit_time = timezone.now() | ||||
referenced_post.save() | ||||
neko259
|
r318 | |||
def _get_page_count(self, thread_count): | ||||
return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE))) | ||||
neko259
|
r291 | |||
neko259
|
r5 | |||
neko259
|
r33 | class TagManager(models.Manager): | ||
neko259
|
r176 | |||
neko259
|
r33 | def get_not_empty_tags(self): | ||
neko259
|
r182 | tags = self.annotate(Count('threads')) \ | ||
.filter(threads__count__gt=0).order_by('name') | ||||
neko259
|
r33 | |||
return tags | ||||
neko259
|
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 | ||||
""" | ||||
neko259
|
r33 | objects = TagManager() | ||
neko259
|
r22 | name = models.CharField(max_length=100) | ||
neko259
|
r182 | threads = models.ManyToManyField('Post', null=True, | ||
blank=True, related_name='tag+') | ||||
neko259
|
r279 | linked = models.ForeignKey('Tag', null=True, blank=True) | ||
neko259
|
r4 | |||
neko259
|
r31 | def __unicode__(self): | ||
return self.name | ||||
neko259
|
r32 | def is_empty(self): | ||
neko259
|
r33 | return self.get_post_count() == 0 | ||
def get_post_count(self): | ||||
neko259
|
r182 | return self.threads.count() | ||
neko259
|
r32 | |||
neko259
|
r57 | def get_popularity(self): | ||
neko259
|
r178 | posts_with_tag = Post.objects.get_threads(tag=self) | ||
neko259
|
r57 | reply_count = 0 | ||
for post in posts_with_tag: | ||||
reply_count += post.get_reply_count() | ||||
neko259
|
r85 | reply_count += OPENING_POST_POPULARITY_WEIGHT | ||
neko259
|
r57 | |||
return reply_count | ||||
neko259
|
r288 | def get_linked_tags(self): | ||
neko259
|
r319 | tag_list = [] | ||
self.get_linked_tags_list(tag_list) | ||||
neko259
|
r288 | |||
neko259
|
r319 | return tag_list | ||
neko259
|
r288 | |||
def get_linked_tags_list(self, tag_list=[]): | ||||
Pavel Ryapolov
|
r286 | """ | ||
Returns the list of tags linked to current. The list can be got | ||||
through returned value or tag_list parameter | ||||
""" | ||||
neko259
|
r319 | |||
neko259
|
r279 | linked_tag = self.linked | ||
Pavel Ryapolov
|
r286 | |||
if linked_tag and not (linked_tag in tag_list): | ||||
tag_list.append(linked_tag) | ||||
neko259
|
r279 | |||
neko259
|
r288 | linked_tag.get_linked_tags_list(tag_list) | ||
neko259
|
r279 | |||
neko259
|
r8 | |||
neko259
|
r2 | class Post(models.Model): | ||
neko259
|
r4 | """A post is a message.""" | ||
neko259
|
r0 | |||
neko259
|
r2 | objects = PostManager() | ||
neko259
|
r0 | |||
neko259
|
r50 | def _update_image_filename(self, filename): | ||
"""Get unique image filename""" | ||||
neko259
|
r71 | path = IMAGES_DIRECTORY | ||
neko259
|
r50 | new_name = str(int(time.mktime(time.gmtime()))) | ||
new_name += str(int(random() * 1000)) | ||||
neko259
|
r71 | new_name += FILE_EXTENSION_DELIMITER | ||
new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0] | ||||
neko259
|
r50 | |||
return os.path.join(path, new_name) | ||||
neko259
|
r79 | title = models.CharField(max_length=TITLE_MAX_LENGTH) | ||
neko259
|
r2 | pub_time = models.DateTimeField() | ||
neko259
|
r79 | text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE, | ||
neko259
|
r90 | escape_html=False) | ||
neko259
|
r127 | |||
neko259
|
r129 | image_width = models.IntegerField(default=0) | ||
image_height = models.IntegerField(default=0) | ||||
neko259
|
r127 | |||
neko259
|
r50 | image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename, | ||
neko259
|
r127 | blank=True, sizes=(IMAGE_THUMB_SIZE,), | ||
width_field='image_width', | ||||
height_field='image_height') | ||||
neko259
|
r116 | poster_ip = models.GenericIPAddressField() | ||
neko259
|
r2 | poster_user_agent = models.TextField() | ||
neko259
|
r169 | |||
neko259
|
r174 | thread = models.ForeignKey('Post', null=True, default=None) | ||
neko259
|
r7 | tags = models.ManyToManyField(Tag) | ||
neko259
|
r25 | last_edit_time = models.DateTimeField() | ||
neko259
|
r186 | bump_time = models.DateTimeField() | ||
neko259
|
r140 | user = models.ForeignKey('User', null=True, default=None) | ||
neko259
|
r0 | |||
neko259
|
r169 | replies = models.ManyToManyField('Post', symmetrical=False, null=True, | ||
blank=True, related_name='re+') | ||||
neko259
|
r325 | referenced_posts = models.ManyToManyField('Post', symmetrical=False, | ||
null=True, | ||||
blank=True, related_name='rfp+') | ||||
neko259
|
r169 | |||
neko259
|
r0 | def __unicode__(self): | ||
Pavel Ryapolov
|
r105 | return '#' + str(self.id) + ' ' + self.title + ' (' + \ | ||
neko259
|
r127 | self.text.raw[:50] + ')' | ||
Ilyas
|
r9 | |||
neko259
|
r147 | def get_title(self): | ||
title = self.title | ||||
if len(title) == 0: | ||||
title = self.text.raw[:20] | ||||
return title | ||||
neko259
|
r30 | def get_reply_count(self): | ||
neko259
|
r169 | return self.replies.count() | ||
neko259
|
r30 | |||
def get_images_count(self): | ||||
images_count = 1 if self.image else 0 | ||||
neko259
|
r177 | images_count += self.replies.filter(image_width__gt=0).count() | ||
neko259
|
r30 | |||
return images_count | ||||
neko259
|
r46 | def can_bump(self): | ||
"""Check if the thread can be bumped by replying""" | ||||
neko259
|
r169 | post_count = self.get_reply_count() + 1 | ||
neko259
|
r46 | |||
neko259
|
r169 | return post_count <= settings.MAX_POSTS_PER_THREAD | ||
neko259
|
r31 | |||
neko259
|
r174 | def bump(self): | ||
neko259
|
r176 | """Bump (move to up) thread""" | ||
neko259
|
r174 | if self.can_bump(): | ||
neko259
|
r186 | self.bump_time = timezone.now() | ||
neko259
|
r174 | |||
neko259
|
r175 | def get_last_replies(self): | ||
neko259
|
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) | ||||
neko259
|
r325 | last_replies = self.replies.all().order_by('pub_time')[ | ||
reply_count - reply_count_to_show:] | ||||
neko259
|
r59 | |||
return last_replies | ||||
neko259
|
r315 | def get_tags(self): | ||
neko259
|
r319 | """Get a sorted tag list""" | ||
neko259
|
r315 | |||
neko259
|
r319 | return self.tags.order_by('name') | ||
neko259
|
r315 | |||
neko259
|
r324 | def get_cache_key(self): | ||
neko259
|
r325 | return str(self.id) + str(self.last_edit_time.microsecond) | ||
neko259
|
r324 | |||
neko259
|
r357 | def get_sorted_referenced_posts(self): | ||
neko259
|
r359 | return self.referenced_posts.order_by('id') | ||
neko259
|
r357 | |||
def is_referenced(self): | ||||
neko259
|
r359 | return self.referenced_posts.count() > 0 | ||
neko259
|
r357 | |||
Ilyas
|
r9 | |||
neko259
|
r121 | class User(models.Model): | ||
user_id = models.CharField(max_length=50) | ||||
rank = models.IntegerField() | ||||
registration_time = models.DateTimeField() | ||||
neko259
|
r144 | fav_tags = models.ManyToManyField(Tag, null=True, blank=True) | ||
fav_threads = models.ManyToManyField(Post, related_name='+', null=True, | ||||
blank=True) | ||||
neko259
|
r121 | |||
def save_setting(self, name, value): | ||||
setting, created = Setting.objects.get_or_create(name=name, user=self) | ||||
neko259
|
r205 | setting.value = str(value) | ||
neko259
|
r121 | setting.save() | ||
return setting | ||||
def get_setting(self, name): | ||||
neko259
|
r161 | if Setting.objects.filter(name=name, user=self).exists(): | ||
setting = Setting.objects.get(name=name, user=self) | ||||
neko259
|
r121 | setting_value = setting.value | ||
else: | ||||
setting_value = None | ||||
return setting_value | ||||
def is_moderator(self): | ||||
return RANK_MODERATOR >= self.rank | ||||
Ilyas
|
r9 | |||
neko259
|
r149 | def get_sorted_fav_tags(self): | ||
neko259
|
r323 | cache_key = self._get_tag_cache_key() | ||
fav_tags = cache.get(cache_key) | ||||
if fav_tags: | ||||
return fav_tags | ||||
neko259
|
r182 | tags = self.fav_tags.annotate(Count('threads'))\ | ||
.filter(threads__count__gt=0).order_by('name') | ||||
neko259
|
r323 | if tags: | ||
cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT) | ||||
neko259
|
r182 | return tags | ||
neko259
|
r149 | |||
neko259
|
r185 | def get_post_count(self): | ||
return Post.objects.filter(user=self).count() | ||||
Ilyas
|
r9 | def __unicode__(self): | ||
neko259
|
r156 | return self.user_id + '(' + str(self.rank) + ')' | ||
neko259
|
r110 | |||
neko259
|
r197 | def get_last_access_time(self): | ||
posts = Post.objects.filter(user=self) | ||||
if posts.count() > 0: | ||||
return posts.latest('pub_time').pub_time | ||||
neko259
|
r323 | 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' | ||||
neko259
|
r140 | |||
neko259
|
r112 | class Setting(models.Model): | ||
neko259
|
r116 | |||
neko259
|
r112 | name = models.CharField(max_length=50) | ||
value = models.CharField(max_length=50) | ||||
user = models.ForeignKey(User) | ||||
neko259
|
r116 | |||
neko259
|
r144 | |||
neko259
|
r116 | class Ban(models.Model): | ||
neko259
|
r176 | |||
neko259
|
r116 | ip = models.GenericIPAddressField() | ||
neko259
|
r340 | reason = models.CharField(default=BAN_REASON_AUTO, | ||
max_length=BAN_REASON_MAX_LENGTH) | ||||
can_read = models.BooleanField(default=True) | ||||
neko259
|
r116 | |||
def __unicode__(self): | ||||
return self.ip | ||||