##// END OF EJS Templates
Bump thread only after adding post to it
neko259 -
r885:ceb6f66d default
parent child Browse files
Show More
@@ -1,447 +1,446 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import logging
3 import logging
4 import re
4 import re
5
5
6 from adjacent import Client
6 from adjacent import Client
7 from django.core.cache import cache
7 from django.core.cache import cache
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.db.models import TextField
10 from django.db.models import TextField
11 from django.template import RequestContext
11 from django.template import RequestContext
12 from django.template.loader import render_to_string
12 from django.template.loader import render_to_string
13 from django.utils import timezone
13 from django.utils import timezone
14
14
15 from boards import settings
15 from boards import settings
16 from boards.mdx_neboard import bbcode_extended
16 from boards.mdx_neboard import bbcode_extended
17 from boards.models import PostImage
17 from boards.models import PostImage
18 from boards.models.base import Viewable
18 from boards.models.base import Viewable
19 from boards.models.thread import Thread
19 from boards.models.thread import Thread
20 from boards.utils import datetime_to_epoch
20 from boards.utils import datetime_to_epoch
21
21
22
22
23 WS_CHANNEL_THREAD = "thread:"
23 WS_CHANNEL_THREAD = "thread:"
24
24
25 APP_LABEL_BOARDS = 'boards'
25 APP_LABEL_BOARDS = 'boards'
26
26
27 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_PPD = 'ppd'
28 CACHE_KEY_POST_URL = 'post_url'
28 CACHE_KEY_POST_URL = 'post_url'
29
29
30 POSTS_PER_DAY_RANGE = 7
30 POSTS_PER_DAY_RANGE = 7
31
31
32 BAN_REASON_AUTO = 'Auto'
32 BAN_REASON_AUTO = 'Auto'
33
33
34 IMAGE_THUMB_SIZE = (200, 150)
34 IMAGE_THUMB_SIZE = (200, 150)
35
35
36 TITLE_MAX_LENGTH = 200
36 TITLE_MAX_LENGTH = 200
37
37
38 # TODO This should be removed
38 # TODO This should be removed
39 NO_IP = '0.0.0.0'
39 NO_IP = '0.0.0.0'
40
40
41 # TODO Real user agent should be saved instead of this
41 # TODO Real user agent should be saved instead of this
42 UNKNOWN_UA = ''
42 UNKNOWN_UA = ''
43
43
44 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
44 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
45
45
46 PARAMETER_TRUNCATED = 'truncated'
46 PARAMETER_TRUNCATED = 'truncated'
47 PARAMETER_TAG = 'tag'
47 PARAMETER_TAG = 'tag'
48 PARAMETER_OFFSET = 'offset'
48 PARAMETER_OFFSET = 'offset'
49 PARAMETER_DIFF_TYPE = 'type'
49 PARAMETER_DIFF_TYPE = 'type'
50
50
51 DIFF_TYPE_HTML = 'html'
51 DIFF_TYPE_HTML = 'html'
52 DIFF_TYPE_JSON = 'json'
52 DIFF_TYPE_JSON = 'json'
53
53
54 PREPARSE_PATTERNS = {
54 PREPARSE_PATTERNS = {
55 r'>>(\d+)': r'[post]\1[/post]',
55 r'>>(\d+)': r'[post]\1[/post]',
56 r'>(.+)': r'[quote]\1[/quote]'
56 r'>(.+)': r'[quote]\1[/quote]'
57 }
57 }
58
58
59
59
60 class PostManager(models.Manager):
60 class PostManager(models.Manager):
61 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
61 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
62 tags=None):
62 tags=None):
63 """
63 """
64 Creates new post
64 Creates new post
65 """
65 """
66
66
67 if not tags:
67 if not tags:
68 tags = []
68 tags = []
69
69
70 posting_time = timezone.now()
70 posting_time = timezone.now()
71 if not thread:
71 if not thread:
72 thread = Thread.objects.create(bump_time=posting_time,
72 thread = Thread.objects.create(bump_time=posting_time,
73 last_edit_time=posting_time)
73 last_edit_time=posting_time)
74 new_thread = True
74 new_thread = True
75 else:
75 else:
76 thread.bump()
77 thread.last_edit_time = posting_time
78 if thread.can_bump() and (
79 thread.get_reply_count() >= settings.MAX_POSTS_PER_THREAD):
80 thread.bumpable = False
81 thread.save()
82 new_thread = False
76 new_thread = False
83
77
84 pre_text = self._preparse_text(text)
78 pre_text = self._preparse_text(text)
85
79
86 post = self.create(title=title,
80 post = self.create(title=title,
87 text=pre_text,
81 text=pre_text,
88 pub_time=posting_time,
82 pub_time=posting_time,
89 thread_new=thread,
83 thread_new=thread,
90 poster_ip=ip,
84 poster_ip=ip,
91 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
85 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
92 # last!
86 # last!
93 last_edit_time=posting_time)
87 last_edit_time=posting_time)
94
88
95 logger = logging.getLogger('boards.post.create')
89 logger = logging.getLogger('boards.post.create')
96
90
97 logger.info('Created post #{} with title "{}" by {}'.format(
91 logger.info('Created post #{} with title "{}" by {}'.format(
98 post.id, post.title, post.poster_ip))
92 post.id, post.title, post.poster_ip))
99
93
100 if image:
94 if image:
101 post_image = PostImage.objects.create(image=image)
95 post_image = PostImage.objects.create(image=image)
102 post.images.add(post_image)
96 post.images.add(post_image)
103 logger.info('Created image #{} for post #{}'.format(
97 logger.info('Created image #{} for post #{}'.format(
104 post_image.id, post.id))
98 post_image.id, post.id))
105
99
106 thread.replies.add(post)
100 thread.replies.add(post)
107 list(map(thread.add_tag, tags))
101 list(map(thread.add_tag, tags))
108
102
109 if new_thread:
103 if new_thread:
110 Thread.objects.process_oldest_threads()
104 Thread.objects.process_oldest_threads()
105 else:
106 thread.bump()
107 thread.last_edit_time = posting_time
108 thread.save()
109
111 self.connect_replies(post)
110 self.connect_replies(post)
112
111
113 return post
112 return post
114
113
115 def delete_posts_by_ip(self, ip):
114 def delete_posts_by_ip(self, ip):
116 """
115 """
117 Deletes all posts of the author with same IP
116 Deletes all posts of the author with same IP
118 """
117 """
119
118
120 posts = self.filter(poster_ip=ip)
119 posts = self.filter(poster_ip=ip)
121 for post in posts:
120 for post in posts:
122 post.delete()
121 post.delete()
123
122
124 def connect_replies(self, post):
123 def connect_replies(self, post):
125 """
124 """
126 Connects replies to a post to show them as a reflink map
125 Connects replies to a post to show them as a reflink map
127 """
126 """
128
127
129 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
128 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
130 post_id = reply_number.group(1)
129 post_id = reply_number.group(1)
131 ref_post = self.filter(id=post_id)
130 ref_post = self.filter(id=post_id)
132 if ref_post.count() > 0:
131 if ref_post.count() > 0:
133 referenced_post = ref_post[0]
132 referenced_post = ref_post[0]
134 referenced_post.referenced_posts.add(post)
133 referenced_post.referenced_posts.add(post)
135 referenced_post.last_edit_time = post.pub_time
134 referenced_post.last_edit_time = post.pub_time
136 referenced_post.build_refmap()
135 referenced_post.build_refmap()
137 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
136 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
138
137
139 referenced_thread = referenced_post.get_thread()
138 referenced_thread = referenced_post.get_thread()
140 referenced_thread.last_edit_time = post.pub_time
139 referenced_thread.last_edit_time = post.pub_time
141 referenced_thread.save(update_fields=['last_edit_time'])
140 referenced_thread.save(update_fields=['last_edit_time'])
142
141
143 def get_posts_per_day(self):
142 def get_posts_per_day(self):
144 """
143 """
145 Gets average count of posts per day for the last 7 days
144 Gets average count of posts per day for the last 7 days
146 """
145 """
147
146
148 day_end = date.today()
147 day_end = date.today()
149 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
148 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
150
149
151 cache_key = CACHE_KEY_PPD + str(day_end)
150 cache_key = CACHE_KEY_PPD + str(day_end)
152 ppd = cache.get(cache_key)
151 ppd = cache.get(cache_key)
153 if ppd:
152 if ppd:
154 return ppd
153 return ppd
155
154
156 day_time_start = timezone.make_aware(datetime.combine(
155 day_time_start = timezone.make_aware(datetime.combine(
157 day_start, dtime()), timezone.get_current_timezone())
156 day_start, dtime()), timezone.get_current_timezone())
158 day_time_end = timezone.make_aware(datetime.combine(
157 day_time_end = timezone.make_aware(datetime.combine(
159 day_end, dtime()), timezone.get_current_timezone())
158 day_end, dtime()), timezone.get_current_timezone())
160
159
161 posts_per_period = float(self.filter(
160 posts_per_period = float(self.filter(
162 pub_time__lte=day_time_end,
161 pub_time__lte=day_time_end,
163 pub_time__gte=day_time_start).count())
162 pub_time__gte=day_time_start).count())
164
163
165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
164 ppd = posts_per_period / POSTS_PER_DAY_RANGE
166
165
167 cache.set(cache_key, ppd)
166 cache.set(cache_key, ppd)
168 return ppd
167 return ppd
169
168
170 def _preparse_text(self, text):
169 def _preparse_text(self, text):
171 """
170 """
172 Preparses text to change patterns like '>>' to a proper bbcode
171 Preparses text to change patterns like '>>' to a proper bbcode
173 tags.
172 tags.
174 """
173 """
175
174
176 for key, value in PREPARSE_PATTERNS.items():
175 for key, value in PREPARSE_PATTERNS.items():
177 text = re.sub(key, value, text)
176 text = re.sub(key, value, text)
178
177
179 return text
178 return text
180
179
181
180
182 class Post(models.Model, Viewable):
181 class Post(models.Model, Viewable):
183 """A post is a message."""
182 """A post is a message."""
184
183
185 objects = PostManager()
184 objects = PostManager()
186
185
187 class Meta:
186 class Meta:
188 app_label = APP_LABEL_BOARDS
187 app_label = APP_LABEL_BOARDS
189 ordering = ('id',)
188 ordering = ('id',)
190
189
191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
190 title = models.CharField(max_length=TITLE_MAX_LENGTH)
192 pub_time = models.DateTimeField()
191 pub_time = models.DateTimeField()
193 text = TextField(blank=True, null=True)
192 text = TextField(blank=True, null=True)
194 _text_rendered = TextField(blank=True, null=True, editable=False)
193 _text_rendered = TextField(blank=True, null=True, editable=False)
195
194
196 images = models.ManyToManyField(PostImage, null=True, blank=True,
195 images = models.ManyToManyField(PostImage, null=True, blank=True,
197 related_name='ip+', db_index=True)
196 related_name='ip+', db_index=True)
198
197
199 poster_ip = models.GenericIPAddressField()
198 poster_ip = models.GenericIPAddressField()
200 poster_user_agent = models.TextField()
199 poster_user_agent = models.TextField()
201
200
202 thread_new = models.ForeignKey('Thread', null=True, default=None,
201 thread_new = models.ForeignKey('Thread', null=True, default=None,
203 db_index=True)
202 db_index=True)
204 last_edit_time = models.DateTimeField()
203 last_edit_time = models.DateTimeField()
205
204
206 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
205 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
207 null=True,
206 null=True,
208 blank=True, related_name='rfp+',
207 blank=True, related_name='rfp+',
209 db_index=True)
208 db_index=True)
210 refmap = models.TextField(null=True, blank=True)
209 refmap = models.TextField(null=True, blank=True)
211
210
212 def __str__(self):
211 def __str__(self):
213 return 'P#{}/{}'.format(self.id, self.title)
212 return 'P#{}/{}'.format(self.id, self.title)
214
213
215 def get_title(self):
214 def get_title(self):
216 """
215 """
217 Gets original post title or part of its text.
216 Gets original post title or part of its text.
218 """
217 """
219
218
220 title = self.title
219 title = self.title
221 if not title:
220 if not title:
222 title = self.get_text()
221 title = self.get_text()
223
222
224 return title
223 return title
225
224
226 def build_refmap(self):
225 def build_refmap(self):
227 """
226 """
228 Builds a replies map string from replies list. This is a cache to stop
227 Builds a replies map string from replies list. This is a cache to stop
229 the server from recalculating the map on every post show.
228 the server from recalculating the map on every post show.
230 """
229 """
231 map_string = ''
230 map_string = ''
232
231
233 first = True
232 first = True
234 for refpost in self.referenced_posts.all():
233 for refpost in self.referenced_posts.all():
235 if not first:
234 if not first:
236 map_string += ', '
235 map_string += ', '
237 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
236 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
238 refpost.id)
237 refpost.id)
239 first = False
238 first = False
240
239
241 self.refmap = map_string
240 self.refmap = map_string
242
241
243 def get_sorted_referenced_posts(self):
242 def get_sorted_referenced_posts(self):
244 return self.refmap
243 return self.refmap
245
244
246 def is_referenced(self):
245 def is_referenced(self):
247 if not self.refmap:
246 if not self.refmap:
248 return False
247 return False
249 else:
248 else:
250 return len(self.refmap) > 0
249 return len(self.refmap) > 0
251
250
252 def is_opening(self):
251 def is_opening(self):
253 """
252 """
254 Checks if this is an opening post or just a reply.
253 Checks if this is an opening post or just a reply.
255 """
254 """
256
255
257 return self.get_thread().get_opening_post_id() == self.id
256 return self.get_thread().get_opening_post_id() == self.id
258
257
259 @transaction.atomic
258 @transaction.atomic
260 def add_tag(self, tag):
259 def add_tag(self, tag):
261 edit_time = timezone.now()
260 edit_time = timezone.now()
262
261
263 thread = self.get_thread()
262 thread = self.get_thread()
264 thread.add_tag(tag)
263 thread.add_tag(tag)
265 self.last_edit_time = edit_time
264 self.last_edit_time = edit_time
266 self.save(update_fields=['last_edit_time'])
265 self.save(update_fields=['last_edit_time'])
267
266
268 thread.last_edit_time = edit_time
267 thread.last_edit_time = edit_time
269 thread.save(update_fields=['last_edit_time'])
268 thread.save(update_fields=['last_edit_time'])
270
269
271 @transaction.atomic
270 @transaction.atomic
272 def remove_tag(self, tag):
271 def remove_tag(self, tag):
273 edit_time = timezone.now()
272 edit_time = timezone.now()
274
273
275 thread = self.get_thread()
274 thread = self.get_thread()
276 thread.remove_tag(tag)
275 thread.remove_tag(tag)
277 self.last_edit_time = edit_time
276 self.last_edit_time = edit_time
278 self.save(update_fields=['last_edit_time'])
277 self.save(update_fields=['last_edit_time'])
279
278
280 thread.last_edit_time = edit_time
279 thread.last_edit_time = edit_time
281 thread.save(update_fields=['last_edit_time'])
280 thread.save(update_fields=['last_edit_time'])
282
281
283 def get_url(self, thread=None):
282 def get_url(self, thread=None):
284 """
283 """
285 Gets full url to the post.
284 Gets full url to the post.
286 """
285 """
287
286
288 cache_key = CACHE_KEY_POST_URL + str(self.id)
287 cache_key = CACHE_KEY_POST_URL + str(self.id)
289 link = cache.get(cache_key)
288 link = cache.get(cache_key)
290
289
291 if not link:
290 if not link:
292 if not thread:
291 if not thread:
293 thread = self.get_thread()
292 thread = self.get_thread()
294
293
295 opening_id = thread.get_opening_post_id()
294 opening_id = thread.get_opening_post_id()
296
295
297 if self.id != opening_id:
296 if self.id != opening_id:
298 link = reverse('thread', kwargs={
297 link = reverse('thread', kwargs={
299 'post_id': opening_id}) + '#' + str(self.id)
298 'post_id': opening_id}) + '#' + str(self.id)
300 else:
299 else:
301 link = reverse('thread', kwargs={'post_id': self.id})
300 link = reverse('thread', kwargs={'post_id': self.id})
302
301
303 cache.set(cache_key, link)
302 cache.set(cache_key, link)
304
303
305 return link
304 return link
306
305
307 def get_thread(self):
306 def get_thread(self):
308 """
307 """
309 Gets post's thread.
308 Gets post's thread.
310 """
309 """
311
310
312 return self.thread_new
311 return self.thread_new
313
312
314 def get_referenced_posts(self):
313 def get_referenced_posts(self):
315 return self.referenced_posts.only('id', 'thread_new')
314 return self.referenced_posts.only('id', 'thread_new')
316
315
317 def get_text(self):
316 def get_text(self):
318 return self.text
317 return self.text
319
318
320 def get_view(self, moderator=False, need_open_link=False,
319 def get_view(self, moderator=False, need_open_link=False,
321 truncated=False, *args, **kwargs):
320 truncated=False, *args, **kwargs):
322 if 'is_opening' in kwargs:
321 if 'is_opening' in kwargs:
323 is_opening = kwargs['is_opening']
322 is_opening = kwargs['is_opening']
324 else:
323 else:
325 is_opening = self.is_opening()
324 is_opening = self.is_opening()
326
325
327 if 'thread' in kwargs:
326 if 'thread' in kwargs:
328 thread = kwargs['thread']
327 thread = kwargs['thread']
329 else:
328 else:
330 thread = self.get_thread()
329 thread = self.get_thread()
331
330
332 if 'can_bump' in kwargs:
331 if 'can_bump' in kwargs:
333 can_bump = kwargs['can_bump']
332 can_bump = kwargs['can_bump']
334 else:
333 else:
335 can_bump = thread.can_bump()
334 can_bump = thread.can_bump()
336
335
337 if is_opening:
336 if is_opening:
338 opening_post_id = self.id
337 opening_post_id = self.id
339 else:
338 else:
340 opening_post_id = thread.get_opening_post_id()
339 opening_post_id = thread.get_opening_post_id()
341
340
342 return render_to_string('boards/post.html', {
341 return render_to_string('boards/post.html', {
343 'post': self,
342 'post': self,
344 'moderator': moderator,
343 'moderator': moderator,
345 'is_opening': is_opening,
344 'is_opening': is_opening,
346 'thread': thread,
345 'thread': thread,
347 'bumpable': can_bump,
346 'bumpable': can_bump,
348 'need_open_link': need_open_link,
347 'need_open_link': need_open_link,
349 'truncated': truncated,
348 'truncated': truncated,
350 'opening_post_id': opening_post_id,
349 'opening_post_id': opening_post_id,
351 })
350 })
352
351
353 def get_first_image(self):
352 def get_first_image(self):
354 return self.images.earliest('id')
353 return self.images.earliest('id')
355
354
356 def delete(self, using=None):
355 def delete(self, using=None):
357 """
356 """
358 Deletes all post images and the post itself. If the post is opening,
357 Deletes all post images and the post itself. If the post is opening,
359 thread with all posts is deleted.
358 thread with all posts is deleted.
360 """
359 """
361
360
362 self.images.all().delete()
361 self.images.all().delete()
363
362
364 if self.is_opening():
363 if self.is_opening():
365 self.get_thread().delete()
364 self.get_thread().delete()
366 else:
365 else:
367 thread = self.get_thread()
366 thread = self.get_thread()
368 thread.last_edit_time = timezone.now()
367 thread.last_edit_time = timezone.now()
369 thread.save()
368 thread.save()
370
369
371 super(Post, self).delete(using)
370 super(Post, self).delete(using)
372
371
373 logging.getLogger('boards.post.delete').info(
372 logging.getLogger('boards.post.delete').info(
374 'Deleted post P#{}/{}'.format(self.id, self.get_title()))
373 'Deleted post P#{}/{}'.format(self.id, self.get_title()))
375
374
376 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
375 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
377 include_last_update=False):
376 include_last_update=False):
378 """
377 """
379 Gets post HTML or JSON data that can be rendered on a page or used by
378 Gets post HTML or JSON data that can be rendered on a page or used by
380 API.
379 API.
381 """
380 """
382
381
383 if format_type == DIFF_TYPE_HTML:
382 if format_type == DIFF_TYPE_HTML:
384 context = RequestContext(request)
383 context = RequestContext(request)
385 context['post'] = self
384 context['post'] = self
386 if PARAMETER_TRUNCATED in request.GET:
385 if PARAMETER_TRUNCATED in request.GET:
387 context[PARAMETER_TRUNCATED] = True
386 context[PARAMETER_TRUNCATED] = True
388
387
389 # TODO Use dict here
388 # TODO Use dict here
390 return render_to_string('boards/api_post.html',
389 return render_to_string('boards/api_post.html',
391 context_instance=context)
390 context_instance=context)
392 elif format_type == DIFF_TYPE_JSON:
391 elif format_type == DIFF_TYPE_JSON:
393 post_json = {
392 post_json = {
394 'id': self.id,
393 'id': self.id,
395 'title': self.title,
394 'title': self.title,
396 'text': self.text.rendered,
395 'text': self.text.rendered,
397 }
396 }
398 if self.images.exists():
397 if self.images.exists():
399 post_image = self.get_first_image()
398 post_image = self.get_first_image()
400 post_json['image'] = post_image.image.url
399 post_json['image'] = post_image.image.url
401 post_json['image_preview'] = post_image.image.url_200x150
400 post_json['image_preview'] = post_image.image.url_200x150
402 if include_last_update:
401 if include_last_update:
403 post_json['bump_time'] = datetime_to_epoch(
402 post_json['bump_time'] = datetime_to_epoch(
404 self.thread_new.bump_time)
403 self.thread_new.bump_time)
405 return post_json
404 return post_json
406
405
407 def send_to_websocket(self, request, recursive=True):
406 def send_to_websocket(self, request, recursive=True):
408 """
407 """
409 Sends post HTML data to the thread web socket.
408 Sends post HTML data to the thread web socket.
410 """
409 """
411
410
412 if not settings.WEBSOCKETS_ENABLED:
411 if not settings.WEBSOCKETS_ENABLED:
413 return
412 return
414
413
415 client = Client()
414 client = Client()
416
415
417 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
416 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
418 client.publish(channel_name, {
417 client.publish(channel_name, {
419 'html': self.get_post_data(
418 'html': self.get_post_data(
420 format_type=DIFF_TYPE_HTML,
419 format_type=DIFF_TYPE_HTML,
421 request=request),
420 request=request),
422 'diff_type': 'added' if recursive else 'updated',
421 'diff_type': 'added' if recursive else 'updated',
423 })
422 })
424 client.send()
423 client.send()
425
424
426 logger = logging.getLogger('boards.post.websocket')
425 logger = logging.getLogger('boards.post.websocket')
427
426
428 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
427 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
429
428
430 if recursive:
429 if recursive:
431 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
430 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
432 post_id = reply_number.group(1)
431 post_id = reply_number.group(1)
433 ref_post = Post.objects.filter(id=post_id)[0]
432 ref_post = Post.objects.filter(id=post_id)[0]
434
433
435 ref_post.send_to_websocket(request, recursive=False)
434 ref_post.send_to_websocket(request, recursive=False)
436
435
437 def save(self, force_insert=False, force_update=False, using=None,
436 def save(self, force_insert=False, force_update=False, using=None,
438 update_fields=None):
437 update_fields=None):
439 self._text_rendered = bbcode_extended(self.get_raw_text())
438 self._text_rendered = bbcode_extended(self.get_raw_text())
440
439
441 super().save(force_insert, force_update, using, update_fields)
440 super().save(force_insert, force_update, using, update_fields)
442
441
443 def get_text(self):
442 def get_text(self):
444 return self._text_rendered
443 return self._text_rendered
445
444
446 def get_raw_text(self):
445 def get_raw_text(self):
447 return self.text
446 return self.text
@@ -1,188 +1,191 b''
1 import logging
1 import logging
2 from django.db.models import Count
2 from django.db.models import Count
3 from django.utils import timezone
3 from django.utils import timezone
4 from django.core.cache import cache
4 from django.core.cache import cache
5 from django.db import models
5 from django.db import models
6 from boards import settings
6 from boards import settings
7
7
8 __author__ = 'neko259'
8 __author__ = 'neko259'
9
9
10
10
11 logger = logging.getLogger(__name__)
11 logger = logging.getLogger(__name__)
12
12
13
13
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
15
15
16
16
17 class ThreadManager(models.Manager):
17 class ThreadManager(models.Manager):
18 def process_oldest_threads(self):
18 def process_oldest_threads(self):
19 """
19 """
20 Preserves maximum thread count. If there are too many threads,
20 Preserves maximum thread count. If there are too many threads,
21 archive or delete the old ones.
21 archive or delete the old ones.
22 """
22 """
23
23
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
25 thread_count = threads.count()
25 thread_count = threads.count()
26
26
27 if thread_count > settings.MAX_THREAD_COUNT:
27 if thread_count > settings.MAX_THREAD_COUNT:
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
29 old_threads = threads[thread_count - num_threads_to_delete:]
29 old_threads = threads[thread_count - num_threads_to_delete:]
30
30
31 for thread in old_threads:
31 for thread in old_threads:
32 if settings.ARCHIVE_THREADS:
32 if settings.ARCHIVE_THREADS:
33 self._archive_thread(thread)
33 self._archive_thread(thread)
34 else:
34 else:
35 thread.delete()
35 thread.delete()
36
36
37 logger.info('Processed %d old threads' % num_threads_to_delete)
37 logger.info('Processed %d old threads' % num_threads_to_delete)
38
38
39 def _archive_thread(self, thread):
39 def _archive_thread(self, thread):
40 thread.archived = True
40 thread.archived = True
41 thread.bumpable = False
41 thread.bumpable = False
42 thread.last_edit_time = timezone.now()
42 thread.last_edit_time = timezone.now()
43 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
43 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
44
44
45
45
46 class Thread(models.Model):
46 class Thread(models.Model):
47 objects = ThreadManager()
47 objects = ThreadManager()
48
48
49 class Meta:
49 class Meta:
50 app_label = 'boards'
50 app_label = 'boards'
51
51
52 tags = models.ManyToManyField('Tag')
52 tags = models.ManyToManyField('Tag')
53 bump_time = models.DateTimeField()
53 bump_time = models.DateTimeField()
54 last_edit_time = models.DateTimeField()
54 last_edit_time = models.DateTimeField()
55 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
55 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
56 blank=True, related_name='tre+')
56 blank=True, related_name='tre+')
57 archived = models.BooleanField(default=False)
57 archived = models.BooleanField(default=False)
58 bumpable = models.BooleanField(default=True)
58 bumpable = models.BooleanField(default=True)
59
59
60 def get_tags(self):
60 def get_tags(self):
61 """
61 """
62 Gets a sorted tag list.
62 Gets a sorted tag list.
63 """
63 """
64
64
65 return self.tags.order_by('name')
65 return self.tags.order_by('name')
66
66
67 def bump(self):
67 def bump(self):
68 """
68 """
69 Bumps (moves to up) thread if possible.
69 Bumps (moves to up) thread if possible.
70 """
70 """
71
71
72 if self.can_bump():
72 if self.can_bump():
73 self.bump_time = timezone.now()
73 self.bump_time = timezone.now()
74
74
75 if self.get_reply_count() >= settings.MAX_POSTS_PER_THREAD:
76 self.bumpable = False
77
75 logger.info('Bumped thread %d' % self.id)
78 logger.info('Bumped thread %d' % self.id)
76
79
77 def get_reply_count(self):
80 def get_reply_count(self):
78 return self.replies.count()
81 return self.replies.count()
79
82
80 def get_images_count(self):
83 def get_images_count(self):
81 # TODO Use sum
84 # TODO Use sum
82 total_count = 0
85 total_count = 0
83 for post_with_image in self.replies.annotate(images_count=Count(
86 for post_with_image in self.replies.annotate(images_count=Count(
84 'images')):
87 'images')):
85 total_count += post_with_image.images_count
88 total_count += post_with_image.images_count
86 return total_count
89 return total_count
87
90
88 def can_bump(self):
91 def can_bump(self):
89 """
92 """
90 Checks if the thread can be bumped by replying to it.
93 Checks if the thread can be bumped by replying to it.
91 """
94 """
92
95
93 return self.bumpable
96 return self.bumpable
94
97
95 def get_last_replies(self):
98 def get_last_replies(self):
96 """
99 """
97 Gets several last replies, not including opening post
100 Gets several last replies, not including opening post
98 """
101 """
99
102
100 if settings.LAST_REPLIES_COUNT > 0:
103 if settings.LAST_REPLIES_COUNT > 0:
101 reply_count = self.get_reply_count()
104 reply_count = self.get_reply_count()
102
105
103 if reply_count > 0:
106 if reply_count > 0:
104 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
107 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
105 reply_count - 1)
108 reply_count - 1)
106 replies = self.get_replies()
109 replies = self.get_replies()
107 last_replies = replies[reply_count - reply_count_to_show:]
110 last_replies = replies[reply_count - reply_count_to_show:]
108
111
109 return last_replies
112 return last_replies
110
113
111 def get_skipped_replies_count(self):
114 def get_skipped_replies_count(self):
112 """
115 """
113 Gets number of posts between opening post and last replies.
116 Gets number of posts between opening post and last replies.
114 """
117 """
115 reply_count = self.get_reply_count()
118 reply_count = self.get_reply_count()
116 last_replies_count = min(settings.LAST_REPLIES_COUNT,
119 last_replies_count = min(settings.LAST_REPLIES_COUNT,
117 reply_count - 1)
120 reply_count - 1)
118 return reply_count - last_replies_count - 1
121 return reply_count - last_replies_count - 1
119
122
120 def get_replies(self, view_fields_only=False):
123 def get_replies(self, view_fields_only=False):
121 """
124 """
122 Gets sorted thread posts
125 Gets sorted thread posts
123 """
126 """
124
127
125 query = self.replies.order_by('pub_time').prefetch_related('images')
128 query = self.replies.order_by('pub_time').prefetch_related('images')
126 if view_fields_only:
129 if view_fields_only:
127 query = query.defer('poster_user_agent', 'text_markup_type')
130 query = query.defer('poster_user_agent', 'text_markup_type')
128 return query.all()
131 return query.all()
129
132
130 def get_replies_with_images(self, view_fields_only=False):
133 def get_replies_with_images(self, view_fields_only=False):
131 return self.get_replies(view_fields_only).annotate(images_count=Count(
134 return self.get_replies(view_fields_only).annotate(images_count=Count(
132 'images')).filter(images_count__gt=0)
135 'images')).filter(images_count__gt=0)
133
136
134 def add_tag(self, tag):
137 def add_tag(self, tag):
135 """
138 """
136 Connects thread to a tag and tag to a thread
139 Connects thread to a tag and tag to a thread
137 """
140 """
138
141
139 self.tags.add(tag)
142 self.tags.add(tag)
140 tag.threads.add(self)
143 tag.threads.add(self)
141
144
142 def remove_tag(self, tag):
145 def remove_tag(self, tag):
143 self.tags.remove(tag)
146 self.tags.remove(tag)
144 tag.threads.remove(self)
147 tag.threads.remove(self)
145
148
146 def get_opening_post(self, only_id=False):
149 def get_opening_post(self, only_id=False):
147 """
150 """
148 Gets the first post of the thread
151 Gets the first post of the thread
149 """
152 """
150
153
151 query = self.replies.order_by('pub_time')
154 query = self.replies.order_by('pub_time')
152 if only_id:
155 if only_id:
153 query = query.only('id')
156 query = query.only('id')
154 opening_post = query.first()
157 opening_post = query.first()
155
158
156 return opening_post
159 return opening_post
157
160
158 def get_opening_post_id(self):
161 def get_opening_post_id(self):
159 """
162 """
160 Gets ID of the first thread post.
163 Gets ID of the first thread post.
161 """
164 """
162
165
163 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
166 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
164 opening_post_id = cache.get(cache_key)
167 opening_post_id = cache.get(cache_key)
165 if not opening_post_id:
168 if not opening_post_id:
166 opening_post_id = self.get_opening_post(only_id=True).id
169 opening_post_id = self.get_opening_post(only_id=True).id
167 cache.set(cache_key, opening_post_id)
170 cache.set(cache_key, opening_post_id)
168
171
169 return opening_post_id
172 return opening_post_id
170
173
171 def __unicode__(self):
174 def __unicode__(self):
172 return str(self.id)
175 return str(self.id)
173
176
174 def get_pub_time(self):
177 def get_pub_time(self):
175 """
178 """
176 Gets opening post's pub time because thread does not have its own one.
179 Gets opening post's pub time because thread does not have its own one.
177 """
180 """
178
181
179 return self.get_opening_post().pub_time
182 return self.get_opening_post().pub_time
180
183
181 def delete(self, using=None):
184 def delete(self, using=None):
182 if self.replies.exists():
185 if self.replies.exists():
183 self.replies.all().delete()
186 self.replies.all().delete()
184
187
185 super(Thread, self).delete(using)
188 super(Thread, self).delete(using)
186
189
187 def __str__(self):
190 def __str__(self):
188 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
191 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
General Comments 0
You need to be logged in to leave comments. Login now