##// END OF EJS Templates
Save bump limit separately for every thread
neko259 -
r1052:a66b11af default
parent child Browse files
Show More
@@ -0,0 +1,20 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0011_notification'),
11 ]
12
13 operations = [
14 migrations.AddField(
15 model_name='thread',
16 name='max_posts',
17 field=models.IntegerField(default=10),
18 preserve_default=True,
19 ),
20 ]
@@ -1,196 +1,197 b''
1 1 import logging
2 2
3 3 from django.db.models import Count, Sum
4 4 from django.utils import timezone
5 5 from django.db import models
6 6
7 7 from boards import settings
8 8 import boards
9 9 from boards.utils import cached_result
10 10 from boards.models.post import Post
11 11
12 12
13 13 __author__ = 'neko259'
14 14
15 15
16 16 logger = logging.getLogger(__name__)
17 17
18 18
19 19 class ThreadManager(models.Manager):
20 20 def process_oldest_threads(self):
21 21 """
22 22 Preserves maximum thread count. If there are too many threads,
23 23 archive or delete the old ones.
24 24 """
25 25
26 26 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
27 27 thread_count = threads.count()
28 28
29 29 if thread_count > settings.MAX_THREAD_COUNT:
30 30 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
31 31 old_threads = threads[thread_count - num_threads_to_delete:]
32 32
33 33 for thread in old_threads:
34 34 if settings.ARCHIVE_THREADS:
35 35 self._archive_thread(thread)
36 36 else:
37 37 thread.delete()
38 38
39 39 logger.info('Processed %d old threads' % num_threads_to_delete)
40 40
41 41 def _archive_thread(self, thread):
42 42 thread.archived = True
43 43 thread.bumpable = False
44 44 thread.last_edit_time = timezone.now()
45 45 thread.update_posts_time()
46 46 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
47 47
48 48
49 49 class Thread(models.Model):
50 50 objects = ThreadManager()
51 51
52 52 class Meta:
53 53 app_label = 'boards'
54 54
55 55 tags = models.ManyToManyField('Tag')
56 56 bump_time = models.DateTimeField(db_index=True)
57 57 last_edit_time = models.DateTimeField()
58 58 archived = models.BooleanField(default=False)
59 59 bumpable = models.BooleanField(default=True)
60 max_posts = models.IntegerField(default=settings.MAX_POSTS_PER_THREAD)
60 61
61 62 def get_tags(self):
62 63 """
63 64 Gets a sorted tag list.
64 65 """
65 66
66 67 return self.tags.order_by('name')
67 68
68 69 def bump(self):
69 70 """
70 71 Bumps (moves to up) thread if possible.
71 72 """
72 73
73 74 if self.can_bump():
74 75 self.bump_time = self.last_edit_time
75 76
76 77 self.update_bump_status()
77 78
78 79 logger.info('Bumped thread %d' % self.id)
79 80
80 81 def update_bump_status(self):
81 if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD:
82 if self.get_reply_count() >= self.max_posts:
82 83 self.bumpable = False
83 84 self.update_posts_time()
84 85
85 86 def get_reply_count(self):
86 87 return self.get_replies().count()
87 88
88 89 def get_images_count(self):
89 90 return self.get_replies().annotate(images_count=Count(
90 91 'images')).aggregate(Sum('images_count'))['images_count__sum']
91 92
92 93 def can_bump(self):
93 94 """
94 95 Checks if the thread can be bumped by replying to it.
95 96 """
96 97
97 98 return self.bumpable and not self.archived
98 99
99 100 def get_last_replies(self):
100 101 """
101 102 Gets several last replies, not including opening post
102 103 """
103 104
104 105 if settings.LAST_REPLIES_COUNT > 0:
105 106 reply_count = self.get_reply_count()
106 107
107 108 if reply_count > 0:
108 109 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
109 110 reply_count - 1)
110 111 replies = self.get_replies()
111 112 last_replies = replies[reply_count - reply_count_to_show:]
112 113
113 114 return last_replies
114 115
115 116 def get_skipped_replies_count(self):
116 117 """
117 118 Gets number of posts between opening post and last replies.
118 119 """
119 120 reply_count = self.get_reply_count()
120 121 last_replies_count = min(settings.LAST_REPLIES_COUNT,
121 122 reply_count - 1)
122 123 return reply_count - last_replies_count - 1
123 124
124 125 def get_replies(self, view_fields_only=False):
125 126 """
126 127 Gets sorted thread posts
127 128 """
128 129
129 130 query = Post.objects.filter(threads__in=[self])
130 131 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
131 132 if view_fields_only:
132 133 query = query.defer('poster_user_agent')
133 134 return query.all()
134 135
135 136 def get_replies_with_images(self, view_fields_only=False):
136 137 """
137 138 Gets replies that have at least one image attached
138 139 """
139 140
140 141 return self.get_replies(view_fields_only).annotate(images_count=Count(
141 142 'images')).filter(images_count__gt=0)
142 143
143 144 def add_tag(self, tag):
144 145 """
145 146 Connects thread to a tag and tag to a thread
146 147 """
147 148
148 149 self.tags.add(tag)
149 150
150 151 def get_opening_post(self, only_id=False):
151 152 """
152 153 Gets the first post of the thread
153 154 """
154 155
155 156 query = self.get_replies().order_by('pub_time')
156 157 if only_id:
157 158 query = query.only('id')
158 159 opening_post = query.first()
159 160
160 161 return opening_post
161 162
162 163 @cached_result
163 164 def get_opening_post_id(self):
164 165 """
165 166 Gets ID of the first thread post.
166 167 """
167 168
168 169 return self.get_opening_post(only_id=True).id
169 170
170 171 def get_pub_time(self):
171 172 """
172 173 Gets opening post's pub time because thread does not have its own one.
173 174 """
174 175
175 176 return self.get_opening_post().pub_time
176 177
177 178 def delete(self, using=None):
178 179 """
179 180 Deletes thread with all replies.
180 181 """
181 182
182 183 for reply in self.get_replies().all():
183 184 reply.delete()
184 185
185 186 super(Thread, self).delete(using)
186 187
187 188 def __str__(self):
188 189 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
189 190
190 191 def get_tag_url_list(self):
191 192 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
192 193
193 194 def update_posts_time(self):
194 195 self.post_set.update(last_edit_time=self.last_edit_time)
195 196 for post in self.post_set.all():
196 197 post.threads.update(last_edit_time=self.last_edit_time)
@@ -1,3 +1,4 b''
1 1 from boards.default_settings import *
2 2
3 # Site-specific settings go here No newline at end of file
3 # Site-specific settings go here
4 MAX_POSTS_PER_THREAD = 12
@@ -1,35 +1,35 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 9 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
10 10 - {{ site_name }}</title>
11 11 {% endblock %}
12 12
13 13 {% block metapanel %}
14 14
15 15 {% get_current_language as LANGUAGE_CODE %}
16 16
17 17 <span class="metapanel"
18 18 data-last-update="{{ last_update }}"
19 19 data-ws-token="{{ ws_token }}"
20 20 data-ws-project="{{ ws_project }}"
21 21 data-ws-host="{{ ws_host }}"
22 22 data-ws-port="{{ ws_port }}">
23 23
24 24 {% block thread_meta_panel %}
25 25 {% endblock %}
26 26
27 27 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
28 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
28 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ thread.max_posts }} {% trans 'messages' %},
29 29 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
30 30 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time|date:'r' }}</time></span>
31 31 [<a href="rss/">RSS</a>]
32 32 {% endcache %}
33 33 </span>
34 34
35 35 {% endblock %}
@@ -1,31 +1,31 b''
1 1 from boards import settings
2 2 from boards.views.thread import ThreadView
3 3
4 4 TEMPLATE_NORMAL = 'boards/thread_normal.html'
5 5
6 6 CONTEXT_OP = 'opening_post'
7 7 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
8 8 CONTEXT_POSTS_LEFT = 'posts_left'
9 9 CONTEXT_BUMPABLE = 'bumpable'
10 10
11 11
12 12 class NormalThreadView(ThreadView):
13 13
14 14 def get_template(self):
15 15 return TEMPLATE_NORMAL
16 16
17 17 def get_data(self, thread):
18 18 params = dict()
19 19
20 20 bumpable = thread.can_bump()
21 21 params[CONTEXT_BUMPABLE] = bumpable
22 max_posts = thread.max_posts
22 23 if bumpable:
23 left_posts = settings.MAX_POSTS_PER_THREAD \
24 - thread.get_reply_count()
24 left_posts = max_posts - thread.get_reply_count()
25 25 params[CONTEXT_POSTS_LEFT] = left_posts
26 26 params[CONTEXT_BUMPLIMIT_PRG] = str(
27 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
27 float(left_posts) / max_posts * 100)
28 28
29 29 params[CONTEXT_OP] = thread.get_opening_post()
30 30
31 31 return params
@@ -1,123 +1,121 b''
1 1 from django.core.exceptions import ObjectDoesNotExist
2 2 from django.http import Http404
3 3 from django.shortcuts import get_object_or_404, render, redirect
4 4 from django.views.generic.edit import FormMixin
5 5
6 6 from boards import utils, settings
7 7 from boards.forms import PostForm, PlainErrorList
8 8 from boards.models import Post
9 9 from boards.views.base import BaseBoardView, CONTEXT_FORM
10 10 from boards.views.posting_mixin import PostMixin
11 11 import neboard
12 12
13 13
14 14 CONTEXT_LASTUPDATE = "last_update"
15 CONTEXT_MAX_REPLIES = 'max_replies'
16 15 CONTEXT_THREAD = 'thread'
17 16 CONTEXT_WS_TOKEN = 'ws_token'
18 17 CONTEXT_WS_PROJECT = 'ws_project'
19 18 CONTEXT_WS_HOST = 'ws_host'
20 19 CONTEXT_WS_PORT = 'ws_port'
21 20
22 21 FORM_TITLE = 'title'
23 22 FORM_TEXT = 'text'
24 23 FORM_IMAGE = 'image'
25 24
26 25
27 26 class ThreadView(BaseBoardView, PostMixin, FormMixin):
28 27
29 28 def get(self, request, post_id, form: PostForm=None):
30 29 try:
31 30 opening_post = Post.objects.get(id=post_id)
32 31 except ObjectDoesNotExist:
33 32 raise Http404
34 33
35 34 # If this is not OP, don't show it as it is
36 35 if not opening_post.is_opening():
37 36 return redirect(opening_post.get_thread().get_opening_post().get_url())
38 37
39 38 if not form:
40 39 form = PostForm(error_class=PlainErrorList)
41 40
42 41 thread_to_show = opening_post.get_thread()
43 42
44 43 params = dict()
45 44
46 45 params[CONTEXT_FORM] = form
47 46 params[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch(
48 47 thread_to_show.last_edit_time))
49 48 params[CONTEXT_THREAD] = thread_to_show
50 params[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
51 49
52 50 if settings.WEBSOCKETS_ENABLED:
53 51 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
54 52 timestamp=params[CONTEXT_LASTUPDATE])
55 53 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
56 54 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
57 55 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
58 56
59 57 params.update(self.get_data(thread_to_show))
60 58
61 59 return render(request, self.get_template(), params)
62 60
63 61 def post(self, request, post_id):
64 62 opening_post = get_object_or_404(Post, id=post_id)
65 63
66 64 # If this is not OP, don't show it as it is
67 65 if not opening_post.is_opening():
68 66 raise Http404
69 67
70 68 if not opening_post.get_thread().archived:
71 69 form = PostForm(request.POST, request.FILES,
72 70 error_class=PlainErrorList)
73 71 form.session = request.session
74 72
75 73 if form.is_valid():
76 74 return self.new_post(request, form, opening_post)
77 75 if form.need_to_ban:
78 76 # Ban user because he is suspected to be a bot
79 77 self._ban_current_user(request)
80 78
81 79 return self.get(request, post_id, form)
82 80
83 81 def new_post(self, request, form: PostForm, opening_post: Post=None,
84 82 html_response=True):
85 83 """
86 84 Adds a new post (in thread or as a reply).
87 85 """
88 86
89 87 ip = utils.get_client_ip(request)
90 88
91 89 data = form.cleaned_data
92 90
93 91 title = data[FORM_TITLE]
94 92 text = data[FORM_TEXT]
95 93 image = form.get_image()
96 94
97 95 text = self._remove_invalid_links(text)
98 96
99 97 post_thread = opening_post.get_thread()
100 98
101 99 post = Post.objects.create_post(title=title, text=text, image=image,
102 100 thread=post_thread, ip=ip)
103 101 post.send_to_websocket(request)
104 102
105 103 if html_response:
106 104 if opening_post:
107 105 return redirect(post.get_url())
108 106 else:
109 107 return post
110 108
111 109 def get_data(self, thread):
112 110 """
113 111 Returns context params for the view.
114 112 """
115 113
116 114 pass
117 115
118 116 def get_template(self):
119 117 """
120 118 Gets template to show the thread mode on.
121 119 """
122 120
123 121 pass
General Comments 0
You need to be logged in to leave comments. Login now