##// END OF EJS Templates
Post URL loading optimizations
neko259 -
r1668:b8867a5d default
parent child Browse files
Show More
@@ -1,414 +1,410 b''
1 1 import uuid
2 2 import hashlib
3 3 import re
4 4
5 5 from boards import settings
6 6 from boards.abstracts.tripcode import Tripcode
7 7 from boards.models import Attachment, KeyPair, GlobalId
8 8 from boards.models.attachment import FILE_TYPES_IMAGE
9 9 from boards.models.base import Viewable
10 10 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
11 11 from boards.models.post.manager import PostManager
12 12 from boards.utils import datetime_to_epoch
13 13 from django.core.exceptions import ObjectDoesNotExist
14 14 from django.core.urlresolvers import reverse
15 15 from django.db import models
16 16 from django.db.models import TextField, QuerySet, F
17 17 from django.template.defaultfilters import truncatewords, striptags
18 18 from django.template.loader import render_to_string
19 19
20 20 CSS_CLS_HIDDEN_POST = 'hidden_post'
21 21 CSS_CLS_DEAD_POST = 'dead_post'
22 22 CSS_CLS_ARCHIVE_POST = 'archive_post'
23 23 CSS_CLS_POST = 'post'
24 24 CSS_CLS_MONOCHROME = 'monochrome'
25 25
26 26 TITLE_MAX_WORDS = 10
27 27
28 28 APP_LABEL_BOARDS = 'boards'
29 29
30 30 BAN_REASON_AUTO = 'Auto'
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
36 36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
37 37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 38
39 39 PARAMETER_TRUNCATED = 'truncated'
40 40 PARAMETER_TAG = 'tag'
41 41 PARAMETER_OFFSET = 'offset'
42 42 PARAMETER_DIFF_TYPE = 'type'
43 43 PARAMETER_CSS_CLASS = 'css_class'
44 44 PARAMETER_THREAD = 'thread'
45 45 PARAMETER_IS_OPENING = 'is_opening'
46 46 PARAMETER_POST = 'post'
47 47 PARAMETER_OP_ID = 'opening_post_id'
48 48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 49 PARAMETER_REPLY_LINK = 'reply_link'
50 50 PARAMETER_NEED_OP_DATA = 'need_op_data'
51 51
52 52 POST_VIEW_PARAMS = (
53 53 'need_op_data',
54 54 'reply_link',
55 55 'need_open_link',
56 56 'truncated',
57 57 'mode_tree',
58 58 'perms',
59 59 'tree_depth',
60 60 )
61 61
62 62
63 63 class Post(models.Model, Viewable):
64 64 """A post is a message."""
65 65
66 66 objects = PostManager()
67 67
68 68 class Meta:
69 69 app_label = APP_LABEL_BOARDS
70 70 ordering = ('id',)
71 71
72 72 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
73 73 pub_time = models.DateTimeField(db_index=True)
74 74 text = TextField(blank=True, null=True)
75 75 _text_rendered = TextField(blank=True, null=True, editable=False)
76 76
77 77 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
78 78 related_name='attachment_posts')
79 79
80 80 poster_ip = models.GenericIPAddressField()
81 81
82 82 # Used for cache and threads updating
83 83 last_edit_time = models.DateTimeField()
84 84
85 85 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
86 86 null=True,
87 87 blank=True, related_name='refposts',
88 88 db_index=True)
89 89 refmap = models.TextField(null=True, blank=True)
90 90 threads = models.ManyToManyField('Thread', db_index=True,
91 91 related_name='multi_replies')
92 92 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
93 93
94 94 url = models.TextField()
95 95 uid = models.TextField(db_index=True)
96 96
97 97 # Global ID with author key. If the message was downloaded from another
98 98 # server, this indicates the server.
99 99 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
100 100 on_delete=models.CASCADE)
101 101
102 102 tripcode = models.CharField(max_length=50, blank=True, default='')
103 103 opening = models.BooleanField(db_index=True)
104 104 hidden = models.BooleanField(default=False)
105 105 version = models.IntegerField(default=1)
106 106
107 107 def __str__(self):
108 108 return 'P#{}/{}'.format(self.id, self.get_title())
109 109
110 110 def get_title(self) -> str:
111 111 return self.title
112 112
113 113 def get_title_or_text(self):
114 114 title = self.get_title()
115 115 if not title:
116 116 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
117 117
118 118 return title
119 119
120 120 def build_refmap(self, excluded_ids=None) -> None:
121 121 """
122 122 Builds a replies map string from replies list. This is a cache to stop
123 123 the server from recalculating the map on every post show.
124 124 """
125 125
126 126 replies = self.referenced_posts
127 127 if excluded_ids is not None:
128 128 replies = replies.exclude(id__in=excluded_ids)
129 129 else:
130 130 replies = replies.all()
131 131
132 132 post_urls = [refpost.get_link_view() for refpost in replies]
133 133
134 134 self.refmap = ', '.join(post_urls)
135 135
136 136 def is_referenced(self) -> bool:
137 137 return self.refmap and len(self.refmap) > 0
138 138
139 139 def is_opening(self) -> bool:
140 140 """
141 141 Checks if this is an opening post or just a reply.
142 142 """
143 143
144 144 return self.opening
145 145
146 146 def get_absolute_url(self, thread=None):
147 url = None
148
149 if thread is None:
150 thread = self.get_thread()
151
152 # Url is cached only for the "main" thread. When getting url
153 # for other threads, do it manually.
154 if self.url:
155 url = self.url
147 # Url is cached only for the "main" thread. When getting url
148 # for other threads, do it manually.
149 url = self.url
156 150
157 151 if url is None:
152 if thread is None:
153 thread = self.get_thread()
158 154 opening = self.is_opening()
159 155 opening_id = self.id if opening else thread.get_opening_post_id()
160 156 url = reverse('thread', kwargs={'post_id': opening_id})
161 157 if not opening:
162 158 url += '#' + str(self.id)
163 159
164 160 return url
165 161
166 162 def get_thread(self):
167 163 return self.thread
168 164
169 165 def get_thread_id(self):
170 166 return self.thread_id
171 167
172 168 def get_threads(self) -> QuerySet:
173 169 """
174 170 Gets post's thread.
175 171 """
176 172
177 173 return self.threads
178 174
179 175 def _get_cache_key(self):
180 176 return [datetime_to_epoch(self.last_edit_time)]
181 177
182 178 def get_view_params(self, *args, **kwargs):
183 179 """
184 180 Gets the parameters required for viewing the post based on the arguments
185 181 given and the post itself.
186 182 """
187 183 thread = self.get_thread()
188 184
189 185 css_classes = [CSS_CLS_POST]
190 186 if thread.is_archived():
191 187 css_classes.append(CSS_CLS_ARCHIVE_POST)
192 188 elif not thread.can_bump():
193 189 css_classes.append(CSS_CLS_DEAD_POST)
194 190 if self.is_hidden():
195 191 css_classes.append(CSS_CLS_HIDDEN_POST)
196 192 if thread.is_monochrome():
197 193 css_classes.append(CSS_CLS_MONOCHROME)
198 194
199 195 params = dict()
200 196 for param in POST_VIEW_PARAMS:
201 197 if param in kwargs:
202 198 params[param] = kwargs[param]
203 199
204 200 params.update({
205 201 PARAMETER_POST: self,
206 202 PARAMETER_IS_OPENING: self.is_opening(),
207 203 PARAMETER_THREAD: thread,
208 204 PARAMETER_CSS_CLASS: ' '.join(css_classes),
209 205 })
210 206
211 207 return params
212 208
213 209 def get_view(self, *args, **kwargs) -> str:
214 210 """
215 211 Renders post's HTML view. Some of the post params can be passed over
216 212 kwargs for the means of caching (if we view the thread, some params
217 213 are same for every post and don't need to be computed over and over.
218 214 """
219 215 params = self.get_view_params(*args, **kwargs)
220 216
221 217 return render_to_string('boards/post.html', params)
222 218
223 219 def get_search_view(self, *args, **kwargs):
224 220 return self.get_view(need_op_data=True, *args, **kwargs)
225 221
226 222 def get_first_image(self) -> Attachment:
227 223 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE).earliest('id')
228 224
229 225 def set_global_id(self, key_pair=None):
230 226 """
231 227 Sets global id based on the given key pair. If no key pair is given,
232 228 default one is used.
233 229 """
234 230
235 231 if key_pair:
236 232 key = key_pair
237 233 else:
238 234 try:
239 235 key = KeyPair.objects.get(primary=True)
240 236 except KeyPair.DoesNotExist:
241 237 # Do not update the global id because there is no key defined
242 238 return
243 239 global_id = GlobalId(key_type=key.key_type,
244 240 key=key.public_key,
245 241 local_id=self.id)
246 242 global_id.save()
247 243
248 244 self.global_id = global_id
249 245
250 246 self.save(update_fields=['global_id'])
251 247
252 248 def get_pub_time_str(self):
253 249 return str(self.pub_time)
254 250
255 251 def get_replied_ids(self):
256 252 """
257 253 Gets ID list of the posts that this post replies.
258 254 """
259 255
260 256 raw_text = self.get_raw_text()
261 257
262 258 local_replied = REGEX_REPLY.findall(raw_text)
263 259 global_replied = []
264 260 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
265 261 key_type = match[0]
266 262 key = match[1]
267 263 local_id = match[2]
268 264
269 265 try:
270 266 global_id = GlobalId.objects.get(key_type=key_type,
271 267 key=key, local_id=local_id)
272 268 for post in Post.objects.filter(global_id=global_id).only('id'):
273 269 global_replied.append(post.id)
274 270 except GlobalId.DoesNotExist:
275 271 pass
276 272 return local_replied + global_replied
277 273
278 274 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
279 275 include_last_update=False) -> str:
280 276 """
281 277 Gets post HTML or JSON data that can be rendered on a page or used by
282 278 API.
283 279 """
284 280
285 281 return get_exporter(format_type).export(self, request,
286 282 include_last_update)
287 283
288 284 def notify_clients(self, recursive=True):
289 285 """
290 286 Sends post HTML data to the thread web socket.
291 287 """
292 288
293 289 if not settings.get_bool('External', 'WebsocketsEnabled'):
294 290 return
295 291
296 292 thread_ids = list()
297 293 for thread in self.get_threads().all():
298 294 thread_ids.append(thread.id)
299 295
300 296 thread.notify_clients()
301 297
302 298 if recursive:
303 299 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
304 300 post_id = reply_number.group(1)
305 301
306 302 try:
307 303 ref_post = Post.objects.get(id=post_id)
308 304
309 305 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
310 306 # If post is in this thread, its thread was already notified.
311 307 # Otherwise, notify its thread separately.
312 308 ref_post.notify_clients(recursive=False)
313 309 except ObjectDoesNotExist:
314 310 pass
315 311
316 312 def build_url(self):
317 313 self.url = self.get_absolute_url()
318 314 self.save(update_fields=['url'])
319 315
320 316 def save(self, force_insert=False, force_update=False, using=None,
321 317 update_fields=None):
322 318 new_post = self.id is None
323 319
324 320 self.uid = str(uuid.uuid4())
325 321 if update_fields is not None and 'uid' not in update_fields:
326 322 update_fields += ['uid']
327 323
328 324 if not new_post:
329 325 for thread in self.get_threads().all():
330 326 thread.last_edit_time = self.last_edit_time
331 327
332 328 thread.save(update_fields=['last_edit_time', 'status'])
333 329
334 330 super().save(force_insert, force_update, using, update_fields)
335 331
336 332 if self.url is None:
337 333 self.build_url()
338 334
339 335 def get_text(self) -> str:
340 336 return self._text_rendered
341 337
342 338 def get_raw_text(self) -> str:
343 339 return self.text
344 340
345 341 def get_sync_text(self) -> str:
346 342 """
347 343 Returns text applicable for sync. It has absolute post reflinks.
348 344 """
349 345
350 346 replacements = dict()
351 347 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
352 348 try:
353 349 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
354 350 replacements[post_id] = absolute_post_id
355 351 except Post.DoesNotExist:
356 352 pass
357 353
358 354 text = self.get_raw_text() or ''
359 355 for key in replacements:
360 356 text = text.replace('[post]{}[/post]'.format(key),
361 357 '[post]{}[/post]'.format(replacements[key]))
362 358 text = text.replace('\r\n', '\n').replace('\r', '\n')
363 359
364 360 return text
365 361
366 362 def connect_threads(self, opening_posts):
367 363 for opening_post in opening_posts:
368 364 threads = opening_post.get_threads().all()
369 365 for thread in threads:
370 366 if thread.can_bump():
371 367 thread.update_bump_status()
372 368
373 369 thread.last_edit_time = self.last_edit_time
374 370 thread.save(update_fields=['last_edit_time', 'status'])
375 371 self.threads.add(opening_post.get_thread())
376 372
377 373 def get_tripcode(self):
378 374 if self.tripcode:
379 375 return Tripcode(self.tripcode)
380 376
381 377 def get_link_view(self):
382 378 """
383 379 Gets view of a reflink to the post.
384 380 """
385 381 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
386 382 self.id)
387 383 if self.is_opening():
388 384 result = '<b>{}</b>'.format(result)
389 385
390 386 return result
391 387
392 388 def is_hidden(self) -> bool:
393 389 return self.hidden
394 390
395 391 def set_hidden(self, hidden):
396 392 self.hidden = hidden
397 393
398 394 def increment_version(self):
399 395 self.version = F('version') + 1
400 396
401 397 def clear_cache(self):
402 398 """
403 399 Clears sync data (content cache, signatures etc).
404 400 """
405 401 global_id = self.global_id
406 402 if global_id is not None and global_id.is_local()\
407 403 and global_id.content is not None:
408 404 global_id.clear_cache()
409 405
410 406 def get_tags(self):
411 407 return self.get_thread().get_tags()
412 408
413 409 def get_ip_color(self):
414 410 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
@@ -1,328 +1,328 b''
1 1 import logging
2 2 from adjacent import Client
3 3 from datetime import timedelta
4 4
5 5
6 6 from django.db.models import Count, Sum, QuerySet, Q
7 7 from django.utils import timezone
8 8 from django.db import models, transaction
9 9
10 10 from boards.models.attachment import FILE_TYPES_IMAGE
11 11 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
12 12
13 13 from boards import settings
14 14 import boards
15 15 from boards.utils import cached_result, datetime_to_epoch
16 16 from boards.models.post import Post
17 17 from boards.models.tag import Tag
18 18
19 19 FAV_THREAD_NO_UPDATES = -1
20 20
21 21
22 22 __author__ = 'neko259'
23 23
24 24
25 25 logger = logging.getLogger(__name__)
26 26
27 27
28 28 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
29 29 WS_NOTIFICATION_TYPE = 'notification_type'
30 30
31 31 WS_CHANNEL_THREAD = "thread:"
32 32
33 33 STATUS_CHOICES = (
34 34 (STATUS_ACTIVE, STATUS_ACTIVE),
35 35 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
36 36 (STATUS_ARCHIVE, STATUS_ARCHIVE),
37 37 )
38 38
39 39
40 40 class ThreadManager(models.Manager):
41 41 def process_old_threads(self):
42 42 """
43 43 Preserves maximum thread count. If there are too many threads,
44 44 archive or delete the old ones.
45 45 """
46 46 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
47 47 old_time = timezone.now() - timedelta(days=old_time_delta)
48 48 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
49 49
50 50 for op in old_ops:
51 51 thread = op.get_thread()
52 52 if settings.get_bool('Storage', 'ArchiveThreads'):
53 53 self._archive_thread(thread)
54 54 else:
55 55 thread.delete()
56 56 logger.info('Processed old thread {}'.format(thread))
57 57
58 58
59 59 def _archive_thread(self, thread):
60 60 thread.status = STATUS_ARCHIVE
61 61 thread.last_edit_time = timezone.now()
62 62 thread.update_posts_time()
63 63 thread.save(update_fields=['last_edit_time', 'status'])
64 64
65 65 def get_new_posts(self, datas):
66 66 query = None
67 67 # TODO Use classes instead of dicts
68 68 for data in datas:
69 69 if data['last_id'] != FAV_THREAD_NO_UPDATES:
70 70 q = (Q(id=data['op'].get_thread_id())
71 71 & Q(multi_replies__id__gt=data['last_id']))
72 72 if query is None:
73 73 query = q
74 74 else:
75 75 query = query | q
76 76 if query is not None:
77 77 return self.filter(query).annotate(
78 78 new_post_count=Count('multi_replies'))
79 79
80 80 def get_new_post_count(self, datas):
81 81 new_posts = self.get_new_posts(datas)
82 82 return new_posts.aggregate(total_count=Count('multi_replies'))\
83 83 ['total_count'] if new_posts else 0
84 84
85 85
86 86 def get_thread_max_posts():
87 87 return settings.get_int('Messages', 'MaxPostsPerThread')
88 88
89 89
90 90 class Thread(models.Model):
91 91 objects = ThreadManager()
92 92
93 93 class Meta:
94 94 app_label = 'boards'
95 95
96 96 tags = models.ManyToManyField('Tag', related_name='thread_tags')
97 97 bump_time = models.DateTimeField(db_index=True)
98 98 last_edit_time = models.DateTimeField()
99 99 max_posts = models.IntegerField(default=get_thread_max_posts)
100 100 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
101 101 choices=STATUS_CHOICES, db_index=True)
102 102 monochrome = models.BooleanField(default=False)
103 103
104 104 def get_tags(self) -> QuerySet:
105 105 """
106 106 Gets a sorted tag list.
107 107 """
108 108
109 109 return self.tags.order_by('name')
110 110
111 111 def bump(self):
112 112 """
113 113 Bumps (moves to up) thread if possible.
114 114 """
115 115
116 116 if self.can_bump():
117 117 self.bump_time = self.last_edit_time
118 118
119 119 self.update_bump_status()
120 120
121 121 logger.info('Bumped thread %d' % self.id)
122 122
123 123 def has_post_limit(self) -> bool:
124 124 return self.max_posts > 0
125 125
126 126 def update_bump_status(self, exclude_posts=None):
127 127 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
128 128 self.status = STATUS_BUMPLIMIT
129 129 self.update_posts_time(exclude_posts=exclude_posts)
130 130
131 131 def _get_cache_key(self):
132 132 return [datetime_to_epoch(self.last_edit_time)]
133 133
134 134 @cached_result(key_method=_get_cache_key)
135 135 def get_reply_count(self) -> int:
136 136 return self.get_replies().count()
137 137
138 138 @cached_result(key_method=_get_cache_key)
139 139 def get_images_count(self) -> int:
140 140 return self.get_replies().filter(
141 141 attachments__mimetype__in=FILE_TYPES_IMAGE)\
142 142 .annotate(images_count=Count(
143 143 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
144 144
145 145 def can_bump(self) -> bool:
146 146 """
147 147 Checks if the thread can be bumped by replying to it.
148 148 """
149 149
150 150 return self.get_status() == STATUS_ACTIVE
151 151
152 152 def get_last_replies(self) -> QuerySet:
153 153 """
154 154 Gets several last replies, not including opening post
155 155 """
156 156
157 157 last_replies_count = settings.get_int('View', 'LastRepliesCount')
158 158
159 159 if last_replies_count > 0:
160 160 reply_count = self.get_reply_count()
161 161
162 162 if reply_count > 0:
163 163 reply_count_to_show = min(last_replies_count,
164 164 reply_count - 1)
165 165 replies = self.get_replies()
166 166 last_replies = replies[reply_count - reply_count_to_show:]
167 167
168 168 return last_replies
169 169
170 170 def get_skipped_replies_count(self) -> int:
171 171 """
172 172 Gets number of posts between opening post and last replies.
173 173 """
174 174 reply_count = self.get_reply_count()
175 175 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
176 176 reply_count - 1)
177 177 return reply_count - last_replies_count - 1
178 178
179 179 # TODO Remove argument, it is not used
180 180 def get_replies(self, view_fields_only=True) -> QuerySet:
181 181 """
182 182 Gets sorted thread posts
183 183 """
184 184 query = self.multi_replies.order_by('pub_time').prefetch_related(
185 185 'thread', 'attachments')
186 186 return query
187 187
188 188 def get_viewable_replies(self) -> QuerySet:
189 189 """
190 190 Gets replies with only fields that are used for viewing.
191 191 """
192 192 return self.get_replies().defer('poster_ip', 'text', 'last_edit_time',
193 193 'version')
194 194
195 195 def get_top_level_replies(self) -> QuerySet:
196 196 return self.get_replies().exclude(refposts__threads__in=[self])
197 197
198 198 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
199 199 """
200 200 Gets replies that have at least one image attached
201 201 """
202 202 return self.get_replies(view_fields_only).filter(
203 203 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
204 204 'attachments')).filter(images_count__gt=0)
205 205
206 206 def get_opening_post(self, only_id=False) -> Post:
207 207 """
208 208 Gets the first post of the thread
209 209 """
210 210
211 211 query = self.get_replies().filter(opening=True)
212 212 if only_id:
213 213 query = query.only('id')
214 214 opening_post = query.first()
215 215
216 216 return opening_post
217 217
218 218 @cached_result()
219 219 def get_opening_post_id(self) -> int:
220 220 """
221 221 Gets ID of the first thread post.
222 222 """
223 223
224 224 return self.get_opening_post(only_id=True).id
225 225
226 226 def get_pub_time(self):
227 227 """
228 228 Gets opening post's pub time because thread does not have its own one.
229 229 """
230 230
231 231 return self.get_opening_post().pub_time
232 232
233 233 def __str__(self):
234 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
234 return 'T#{}'.format(self.id)
235 235
236 236 def get_tag_url_list(self) -> list:
237 237 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
238 238
239 239 def update_posts_time(self, exclude_posts=None):
240 240 last_edit_time = self.last_edit_time
241 241
242 242 for post in self.multi_replies.all():
243 243 if exclude_posts is None or post not in exclude_posts:
244 244 # Manual update is required because uids are generated on save
245 245 post.last_edit_time = last_edit_time
246 246 post.save(update_fields=['last_edit_time'])
247 247
248 248 post.get_threads().update(last_edit_time=last_edit_time)
249 249
250 250 def notify_clients(self):
251 251 if not settings.get_bool('External', 'WebsocketsEnabled'):
252 252 return
253 253
254 254 client = Client()
255 255
256 256 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
257 257 client.publish(channel_name, {
258 258 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
259 259 })
260 260 client.send()
261 261
262 262 def get_absolute_url(self):
263 263 return self.get_opening_post().get_absolute_url()
264 264
265 265 def get_required_tags(self):
266 266 return self.get_tags().filter(required=True)
267 267
268 268 def get_replies_newer(self, post_id):
269 269 return self.get_replies().filter(id__gt=post_id)
270 270
271 271 def is_archived(self):
272 272 return self.get_status() == STATUS_ARCHIVE
273 273
274 274 def get_status(self):
275 275 return self.status
276 276
277 277 def is_monochrome(self):
278 278 return self.monochrome
279 279
280 280 # If tags have parent, add them to the tag list
281 281 @transaction.atomic
282 282 def refresh_tags(self):
283 283 for tag in self.get_tags().all():
284 284 parents = tag.get_all_parents()
285 285 if len(parents) > 0:
286 286 self.tags.add(*parents)
287 287
288 288 def get_reply_tree(self):
289 289 replies = self.get_replies().prefetch_related('refposts')
290 290 tree = []
291 291 for reply in replies:
292 292 parents = reply.refposts.all()
293 293
294 294 found_parent = False
295 295 searching_for_index = False
296 296
297 297 if len(parents) > 0:
298 298 index = 0
299 299 parent_depth = 0
300 300
301 301 indexes_to_insert = []
302 302
303 303 for depth, element in tree:
304 304 index += 1
305 305
306 306 # If this element is next after parent on the same level,
307 307 # insert child before it
308 308 if searching_for_index and depth <= parent_depth:
309 309 indexes_to_insert.append((index - 1, parent_depth))
310 310 searching_for_index = False
311 311
312 312 if element in parents:
313 313 found_parent = True
314 314 searching_for_index = True
315 315 parent_depth = depth
316 316
317 317 if not found_parent:
318 318 tree.append((0, reply))
319 319 else:
320 320 if searching_for_index:
321 321 tree.append((parent_depth + 1, reply))
322 322
323 323 offset = 0
324 324 for last_index, parent_depth in indexes_to_insert:
325 325 tree.insert(last_index + offset, (parent_depth + 1, reply))
326 326 offset += 1
327 327
328 328 return tree
General Comments 0
You need to be logged in to leave comments. Login now