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