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