##// END OF EJS Templates
Fixed issues related to removal of multithread posts
neko259 -
r1708:a6b599ad default
parent child Browse files
Show More
@@ -1,393 +1,386 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, NO_IP
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 thread = models.ForeignKey('Thread', db_index=True, related_name='replies')
91 91
92 92 url = models.TextField()
93 93 uid = models.TextField(db_index=True)
94 94
95 95 # Global ID with author key. If the message was downloaded from another
96 96 # server, this indicates the server.
97 97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
98 98 on_delete=models.CASCADE)
99 99
100 100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 101 opening = models.BooleanField(db_index=True)
102 102 hidden = models.BooleanField(default=False)
103 103 version = models.IntegerField(default=1)
104 104
105 105 def __str__(self):
106 106 return 'P#{}/{}'.format(self.id, self.get_title())
107 107
108 108 def get_title(self) -> str:
109 109 return self.title
110 110
111 111 def get_title_or_text(self):
112 112 title = self.get_title()
113 113 if not title:
114 114 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
115 115
116 116 return title
117 117
118 118 def build_refmap(self, excluded_ids=None) -> None:
119 119 """
120 120 Builds a replies map string from replies list. This is a cache to stop
121 121 the server from recalculating the map on every post show.
122 122 """
123 123
124 124 replies = self.referenced_posts
125 125 if excluded_ids is not None:
126 126 replies = replies.exclude(id__in=excluded_ids)
127 127 else:
128 128 replies = replies.all()
129 129
130 130 post_urls = [refpost.get_link_view() for refpost in replies]
131 131
132 132 self.refmap = ', '.join(post_urls)
133 133
134 134 def is_referenced(self) -> bool:
135 135 return self.refmap and len(self.refmap) > 0
136 136
137 137 def is_opening(self) -> bool:
138 138 """
139 139 Checks if this is an opening post or just a reply.
140 140 """
141 141
142 142 return self.opening
143 143
144 144 def get_absolute_url(self, thread=None):
145 145 # Url is cached only for the "main" thread. When getting url
146 146 # for other threads, do it manually.
147 147 return self.url
148 148
149 149 def get_thread(self):
150 150 return self.thread
151 151
152 152 def get_thread_id(self):
153 153 return self.thread_id
154 154
155 def get_threads(self) -> QuerySet:
156 """
157 Gets post's thread.
158 """
159
160 return self.threads
161
162 155 def _get_cache_key(self):
163 156 return [datetime_to_epoch(self.last_edit_time)]
164 157
165 158 def get_view_params(self, *args, **kwargs):
166 159 """
167 160 Gets the parameters required for viewing the post based on the arguments
168 161 given and the post itself.
169 162 """
170 163 thread = kwargs.get('thread') or self.get_thread()
171 164
172 165 css_classes = [CSS_CLS_POST]
173 166 if thread.is_archived():
174 167 css_classes.append(CSS_CLS_ARCHIVE_POST)
175 168 elif not thread.can_bump():
176 169 css_classes.append(CSS_CLS_DEAD_POST)
177 170 if self.is_hidden():
178 171 css_classes.append(CSS_CLS_HIDDEN_POST)
179 172 if thread.is_monochrome():
180 173 css_classes.append(CSS_CLS_MONOCHROME)
181 174
182 175 params = dict()
183 176 for param in POST_VIEW_PARAMS:
184 177 if param in kwargs:
185 178 params[param] = kwargs[param]
186 179
187 180 params.update({
188 181 PARAMETER_POST: self,
189 182 PARAMETER_IS_OPENING: self.is_opening(),
190 183 PARAMETER_THREAD: thread,
191 184 PARAMETER_CSS_CLASS: ' '.join(css_classes),
192 185 })
193 186
194 187 return params
195 188
196 189 def get_view(self, *args, **kwargs) -> str:
197 190 """
198 191 Renders post's HTML view. Some of the post params can be passed over
199 192 kwargs for the means of caching (if we view the thread, some params
200 193 are same for every post and don't need to be computed over and over.
201 194 """
202 195 params = self.get_view_params(*args, **kwargs)
203 196
204 197 return render_to_string('boards/post.html', params)
205 198
206 199 def get_search_view(self, *args, **kwargs):
207 200 return self.get_view(need_op_data=True, *args, **kwargs)
208 201
209 202 def get_first_image(self) -> Attachment:
210 203 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE).earliest('id')
211 204
212 205 def set_global_id(self, key_pair=None):
213 206 """
214 207 Sets global id based on the given key pair. If no key pair is given,
215 208 default one is used.
216 209 """
217 210
218 211 if key_pair:
219 212 key = key_pair
220 213 else:
221 214 try:
222 215 key = KeyPair.objects.get(primary=True)
223 216 except KeyPair.DoesNotExist:
224 217 # Do not update the global id because there is no key defined
225 218 return
226 219 global_id = GlobalId(key_type=key.key_type,
227 220 key=key.public_key,
228 221 local_id=self.id)
229 222 global_id.save()
230 223
231 224 self.global_id = global_id
232 225
233 226 self.save(update_fields=['global_id'])
234 227
235 228 def get_pub_time_str(self):
236 229 return str(self.pub_time)
237 230
238 231 def get_replied_ids(self):
239 232 """
240 233 Gets ID list of the posts that this post replies.
241 234 """
242 235
243 236 raw_text = self.get_raw_text()
244 237
245 238 local_replied = REGEX_REPLY.findall(raw_text)
246 239 global_replied = []
247 240 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
248 241 key_type = match[0]
249 242 key = match[1]
250 243 local_id = match[2]
251 244
252 245 try:
253 246 global_id = GlobalId.objects.get(key_type=key_type,
254 247 key=key, local_id=local_id)
255 248 for post in Post.objects.filter(global_id=global_id).only('id'):
256 249 global_replied.append(post.id)
257 250 except GlobalId.DoesNotExist:
258 251 pass
259 252 return local_replied + global_replied
260 253
261 254 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
262 255 include_last_update=False) -> str:
263 256 """
264 257 Gets post HTML or JSON data that can be rendered on a page or used by
265 258 API.
266 259 """
267 260
268 261 return get_exporter(format_type).export(self, request,
269 262 include_last_update)
270 263
271 264 def notify_clients(self, recursive=True):
272 265 """
273 266 Sends post HTML data to the thread web socket.
274 267 """
275 268
276 269 if not settings.get_bool('External', 'WebsocketsEnabled'):
277 270 return
278 271
279 272 thread_ids = list()
280 273 self.get_thread().notify_clients()
281 274
282 275 if recursive:
283 276 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
284 277 post_id = reply_number.group(1)
285 278
286 279 try:
287 280 ref_post = Post.objects.get(id=post_id)
288 281
289 282 if ref_post.get_thread().id not in thread_ids:
290 283 # If post is in this thread, its thread was already notified.
291 284 # Otherwise, notify its thread separately.
292 285 ref_post.notify_clients(recursive=False)
293 286 except ObjectDoesNotExist:
294 287 pass
295 288
296 289 def _build_url(self):
297 290 opening = self.is_opening()
298 291 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
299 292 url = reverse('thread', kwargs={'post_id': opening_id})
300 293 if not opening:
301 294 url += '#' + str(self.id)
302 295
303 296 return url
304 297
305 298 def save(self, force_insert=False, force_update=False, using=None,
306 299 update_fields=None):
307 300 new_post = self.id is None
308 301
309 302 self.uid = str(uuid.uuid4())
310 303 if update_fields is not None and 'uid' not in update_fields:
311 304 update_fields += ['uid']
312 305
313 306 if not new_post:
314 307 thread = self.get_thread()
315 308 if thread:
316 309 thread.last_edit_time = self.last_edit_time
317 310 thread.save(update_fields=['last_edit_time', 'status'])
318 311
319 312 super().save(force_insert, force_update, using, update_fields)
320 313
321 314 if new_post:
322 315 self.url = self._build_url()
323 316 super().save(update_fields=['url'])
324 317
325 318 def get_text(self) -> str:
326 319 return self._text_rendered
327 320
328 321 def get_raw_text(self) -> str:
329 322 return self.text
330 323
331 324 def get_sync_text(self) -> str:
332 325 """
333 326 Returns text applicable for sync. It has absolute post reflinks.
334 327 """
335 328
336 329 replacements = dict()
337 330 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
338 331 try:
339 332 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
340 333 replacements[post_id] = absolute_post_id
341 334 except Post.DoesNotExist:
342 335 pass
343 336
344 337 text = self.get_raw_text() or ''
345 338 for key in replacements:
346 339 text = text.replace('[post]{}[/post]'.format(key),
347 340 '[post]{}[/post]'.format(replacements[key]))
348 341 text = text.replace('\r\n', '\n').replace('\r', '\n')
349 342
350 343 return text
351 344
352 345 def get_tripcode(self):
353 346 if self.tripcode:
354 347 return Tripcode(self.tripcode)
355 348
356 349 def get_link_view(self):
357 350 """
358 351 Gets view of a reflink to the post.
359 352 """
360 353 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
361 354 self.id)
362 355 if self.is_opening():
363 356 result = '<b>{}</b>'.format(result)
364 357
365 358 return result
366 359
367 360 def is_hidden(self) -> bool:
368 361 return self.hidden
369 362
370 363 def set_hidden(self, hidden):
371 364 self.hidden = hidden
372 365
373 366 def increment_version(self):
374 367 self.version = F('version') + 1
375 368
376 369 def clear_cache(self):
377 370 """
378 371 Clears sync data (content cache, signatures etc).
379 372 """
380 373 global_id = self.global_id
381 374 if global_id is not None and global_id.is_local()\
382 375 and global_id.content is not None:
383 376 global_id.clear_cache()
384 377
385 378 def get_tags(self):
386 379 return self.get_thread().get_tags()
387 380
388 381 def get_ip_color(self):
389 382 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
390 383
391 384 def has_ip(self):
392 385 return self.poster_ip != NO_IP
393 386
@@ -1,327 +1,325 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(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('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('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.replies.order_by('pub_time').prefetch_related(
185 185 '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('text', 'last_edit_time', 'version')
193 193
194 194 def get_top_level_replies(self) -> QuerySet:
195 195 return self.get_replies().exclude(refposts__threads__in=[self])
196 196
197 197 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
198 198 """
199 199 Gets replies that have at least one image attached
200 200 """
201 201 return self.get_replies(view_fields_only).filter(
202 202 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
203 203 'attachments')).filter(images_count__gt=0)
204 204
205 205 def get_opening_post(self, only_id=False) -> Post:
206 206 """
207 207 Gets the first post of the thread
208 208 """
209 209
210 210 query = self.get_replies().filter(opening=True)
211 211 if only_id:
212 212 query = query.only('id')
213 213 opening_post = query.first()
214 214
215 215 return opening_post
216 216
217 217 @cached_result()
218 218 def get_opening_post_id(self) -> int:
219 219 """
220 220 Gets ID of the first thread post.
221 221 """
222 222
223 223 return self.get_opening_post(only_id=True).id
224 224
225 225 def get_pub_time(self):
226 226 """
227 227 Gets opening post's pub time because thread does not have its own one.
228 228 """
229 229
230 230 return self.get_opening_post().pub_time
231 231
232 232 def __str__(self):
233 233 return 'T#{}'.format(self.id)
234 234
235 235 def get_tag_url_list(self) -> list:
236 236 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
237 237
238 238 def update_posts_time(self, exclude_posts=None):
239 239 last_edit_time = self.last_edit_time
240 240
241 for post in self.multi_replies.all():
241 for post in self.replies.all():
242 242 if exclude_posts is None or post not in exclude_posts:
243 243 # Manual update is required because uids are generated on save
244 244 post.last_edit_time = last_edit_time
245 245 post.save(update_fields=['last_edit_time'])
246 246
247 post.get_threads().update(last_edit_time=last_edit_time)
248
249 247 def notify_clients(self):
250 248 if not settings.get_bool('External', 'WebsocketsEnabled'):
251 249 return
252 250
253 251 client = Client()
254 252
255 253 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
256 254 client.publish(channel_name, {
257 255 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
258 256 })
259 257 client.send()
260 258
261 259 def get_absolute_url(self):
262 260 return self.get_opening_post().get_absolute_url()
263 261
264 262 def get_required_tags(self):
265 263 return self.get_tags().filter(required=True)
266 264
267 265 def get_replies_newer(self, post_id):
268 266 return self.get_replies().filter(id__gt=post_id)
269 267
270 268 def is_archived(self):
271 269 return self.get_status() == STATUS_ARCHIVE
272 270
273 271 def get_status(self):
274 272 return self.status
275 273
276 274 def is_monochrome(self):
277 275 return self.monochrome
278 276
279 277 # If tags have parent, add them to the tag list
280 278 @transaction.atomic
281 279 def refresh_tags(self):
282 280 for tag in self.get_tags().all():
283 281 parents = tag.get_all_parents()
284 282 if len(parents) > 0:
285 283 self.tags.add(*parents)
286 284
287 285 def get_reply_tree(self):
288 286 replies = self.get_replies().prefetch_related('refposts')
289 287 tree = []
290 288 for reply in replies:
291 289 parents = reply.refposts.all()
292 290
293 291 found_parent = False
294 292 searching_for_index = False
295 293
296 294 if len(parents) > 0:
297 295 index = 0
298 296 parent_depth = 0
299 297
300 298 indexes_to_insert = []
301 299
302 300 for depth, element in tree:
303 301 index += 1
304 302
305 303 # If this element is next after parent on the same level,
306 304 # insert child before it
307 305 if searching_for_index and depth <= parent_depth:
308 306 indexes_to_insert.append((index - 1, parent_depth))
309 307 searching_for_index = False
310 308
311 309 if element in parents:
312 310 found_parent = True
313 311 searching_for_index = True
314 312 parent_depth = depth
315 313
316 314 if not found_parent:
317 315 tree.append((0, reply))
318 316 else:
319 317 if searching_for_index:
320 318 tree.append((parent_depth + 1, reply))
321 319
322 320 offset = 0
323 321 for last_index, parent_depth in indexes_to_insert:
324 322 tree.insert(last_index + offset, (parent_depth + 1, reply))
325 323 offset += 1
326 324
327 325 return tree
General Comments 0
You need to be logged in to leave comments. Login now