##// END OF EJS Templates
Fixed loading of first level posts in tree view
neko259 -
r2028:4249fd1b default
parent child Browse files
Show More
@@ -1,326 +1,326
1 1 import logging
2 2 from datetime import timedelta
3 3
4 4 from django.db import models, transaction
5 5 from django.db.models import Count, Sum, QuerySet, Q
6 6 from django.utils import timezone
7 7
8 8 import boards
9 9 from boards import settings
10 10 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
11 11 from boards.models.attachment import FILE_TYPES_IMAGE
12 12 from boards.models.post import Post
13 13 from boards.models.tag import Tag, TagAlias
14 14 from boards.settings import SECTION_VIEW
15 15 from boards.utils import cached_result, datetime_to_epoch
16 16
17 17 FAV_THREAD_NO_UPDATES = -1
18 18
19 19
20 20 __author__ = 'neko259'
21 21
22 22
23 23 logger = logging.getLogger(__name__)
24 24
25 25
26 26 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
27 27 WS_NOTIFICATION_TYPE = 'notification_type'
28 28
29 29 WS_CHANNEL_THREAD = "thread:"
30 30
31 31 STATUS_CHOICES = (
32 32 (STATUS_ACTIVE, STATUS_ACTIVE),
33 33 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
34 34 (STATUS_ARCHIVE, STATUS_ARCHIVE),
35 35 )
36 36
37 37
38 38 class ThreadManager(models.Manager):
39 39 def process_old_threads(self):
40 40 """
41 41 Preserves maximum thread count. If there are too many threads,
42 42 archive or delete the old ones.
43 43 """
44 44 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
45 45 old_time = timezone.now() - timedelta(days=old_time_delta)
46 46 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
47 47
48 48 for op in old_ops:
49 49 thread = op.get_thread()
50 50 if settings.get_bool('Storage', 'ArchiveThreads'):
51 51 self._archive_thread(thread)
52 52 else:
53 53 thread.delete()
54 54 logger.info('Processed old thread {}'.format(thread))
55 55
56 56
57 57 def _archive_thread(self, thread):
58 58 thread.status = STATUS_ARCHIVE
59 59 thread.last_edit_time = timezone.now()
60 60 thread.update_posts_time()
61 61 thread.save(update_fields=['last_edit_time', 'status'])
62 62
63 63 def get_new_posts(self, datas):
64 64 query = None
65 65 # TODO Use classes instead of dicts
66 66 for data in datas:
67 67 if data['last_id'] != FAV_THREAD_NO_UPDATES:
68 68 q = (Q(id=data['op'].get_thread_id())
69 69 & Q(replies__id__gt=data['last_id']))
70 70 if query is None:
71 71 query = q
72 72 else:
73 73 query = query | q
74 74 if query is not None:
75 75 return self.filter(query).annotate(
76 76 new_post_count=Count('replies'))
77 77
78 78 def get_new_post_count(self, datas):
79 79 new_posts = self.get_new_posts(datas)
80 80 return new_posts.aggregate(total_count=Count('replies'))\
81 81 ['total_count'] if new_posts else 0
82 82
83 83
84 84 def get_thread_max_posts():
85 85 return settings.get_int('Messages', 'MaxPostsPerThread')
86 86
87 87
88 88 class Thread(models.Model):
89 89 objects = ThreadManager()
90 90
91 91 class Meta:
92 92 app_label = 'boards'
93 93
94 94 tags = models.ManyToManyField('Tag', related_name='thread_tags')
95 95 bump_time = models.DateTimeField(db_index=True)
96 96 last_edit_time = models.DateTimeField()
97 97 max_posts = models.IntegerField(default=get_thread_max_posts)
98 98 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
99 99 choices=STATUS_CHOICES, db_index=True)
100 100 monochrome = models.BooleanField(default=False)
101 101 stickerpack = models.BooleanField(default=False)
102 102
103 103 def get_tags(self) -> QuerySet:
104 104 """
105 105 Gets a sorted tag list.
106 106 """
107 107
108 108 return self.tags.filter(aliases__in=TagAlias.objects.filter_localized(parent__thread_tags=self)).order_by('aliases__name')
109 109
110 110 def bump(self):
111 111 """
112 112 Bumps (moves to up) thread if possible.
113 113 """
114 114
115 115 if self.can_bump():
116 116 self.bump_time = self.last_edit_time
117 117
118 118 self.update_bump_status()
119 119
120 120 logger.info('Bumped thread %d' % self.id)
121 121
122 122 def has_post_limit(self) -> bool:
123 123 return self.max_posts > 0
124 124
125 125 def update_bump_status(self, exclude_posts=None):
126 126 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
127 127 self.status = STATUS_BUMPLIMIT
128 128 self.update_posts_time(exclude_posts=exclude_posts)
129 129
130 130 def _get_cache_key(self):
131 131 return [datetime_to_epoch(self.last_edit_time)]
132 132
133 133 @cached_result(key_method=_get_cache_key)
134 134 def get_reply_count(self) -> int:
135 135 return self.get_replies().count()
136 136
137 137 @cached_result(key_method=_get_cache_key)
138 138 def get_images_count(self) -> int:
139 139 return self.get_replies().filter(
140 140 attachments__mimetype__in=FILE_TYPES_IMAGE)\
141 141 .annotate(images_count=Count(
142 142 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
143 143
144 144 @cached_result(key_method=_get_cache_key)
145 145 def get_attachment_count(self) -> int:
146 146 return self.get_replies().annotate(attachment_count=Count('attachments'))\
147 147 .aggregate(Sum('attachment_count'))['attachment_count__sum'] or 0
148 148
149 149 def can_bump(self) -> bool:
150 150 """
151 151 Checks if the thread can be bumped by replying to it.
152 152 """
153 153
154 154 return self.get_status() == STATUS_ACTIVE
155 155
156 156 def get_last_replies(self) -> QuerySet:
157 157 """
158 158 Gets several last replies, not including opening post
159 159 """
160 160
161 161 last_replies_count = settings.get_int(SECTION_VIEW, 'LastRepliesCount')
162 162
163 163 if last_replies_count > 0:
164 164 reply_count = self.get_reply_count()
165 165
166 166 if reply_count > 0:
167 167 reply_count_to_show = min(last_replies_count,
168 168 reply_count - 1)
169 169 replies = self.get_replies()
170 170 last_replies = replies[reply_count - reply_count_to_show:]
171 171
172 172 return last_replies
173 173
174 174 def get_skipped_replies_count(self) -> int:
175 175 """
176 176 Gets number of posts between opening post and last replies.
177 177 """
178 178 reply_count = self.get_reply_count()
179 179 last_replies_count = min(settings.get_int(SECTION_VIEW, 'LastRepliesCount'),
180 180 reply_count - 1)
181 181 return reply_count - last_replies_count - 1
182 182
183 183 # TODO Remove argument, it is not used
184 184 def get_replies(self, view_fields_only=True) -> QuerySet:
185 185 """
186 186 Gets sorted thread posts
187 187 """
188 188 query = self.replies.order_by('pub_time').prefetch_related(
189 189 'attachments')
190 190 return query
191 191
192 192 def get_viewable_replies(self) -> QuerySet:
193 193 """
194 194 Gets replies with only fields that are used for viewing.
195 195 """
196 196 return self.get_replies().defer('text', 'last_edit_time')
197 197
198 198 def get_top_level_replies(self) -> QuerySet:
199 199 return self.get_replies().exclude(refposts__threads__in=[self])
200 200
201 201 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
202 202 """
203 203 Gets replies that have at least one image attached
204 204 """
205 205 return self.get_replies(view_fields_only).filter(
206 206 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
207 207 'attachments')).filter(images_count__gt=0)
208 208
209 209 def get_opening_post(self, only_id=False) -> Post:
210 210 """
211 211 Gets the first post of the thread
212 212 """
213 213
214 214 query = self.get_replies().filter(opening=True)
215 215 if only_id:
216 216 query = query.only('id')
217 217 opening_post = query.first()
218 218
219 219 return opening_post
220 220
221 221 @cached_result()
222 222 def get_opening_post_id(self) -> int:
223 223 """
224 224 Gets ID of the first thread post.
225 225 """
226 226
227 227 return self.get_opening_post(only_id=True).id
228 228
229 229 def get_pub_time(self):
230 230 """
231 231 Gets opening post's pub time because thread does not have its own one.
232 232 """
233 233
234 234 return self.get_opening_post().pub_time
235 235
236 236 def __str__(self):
237 237 return 'T#{}/{}'.format(self.id, self.get_opening_post())
238 238
239 239 def get_tag_url_list(self) -> list:
240 240 return boards.models.Tag.objects.get_tag_url_list(self.get_tags().all())
241 241
242 242 def update_posts_time(self, exclude_posts=None):
243 243 last_edit_time = self.last_edit_time
244 244
245 245 for post in self.replies.all():
246 246 if exclude_posts is None or post not in exclude_posts:
247 247 # Manual update is required because uids are generated on save
248 248 post.last_edit_time = last_edit_time
249 249 post.save(update_fields=['last_edit_time'])
250 250
251 251 def get_absolute_url(self):
252 252 return self.get_opening_post().get_absolute_url()
253 253
254 254 def get_required_tags(self):
255 255 return self.get_tags().filter(required=True)
256 256
257 257 def get_sections_str(self):
258 258 return Tag.objects.get_tag_url_list(self.get_required_tags())
259 259
260 260 def get_replies_newer(self, post_id):
261 261 return self.get_replies().filter(id__gt=post_id)
262 262
263 263 def is_archived(self):
264 264 return self.get_status() == STATUS_ARCHIVE
265 265
266 266 def is_bumplimit(self):
267 267 return self.get_status() == STATUS_BUMPLIMIT
268 268
269 269 def get_status(self):
270 270 return self.status
271 271
272 272 def is_monochrome(self):
273 273 return self.monochrome
274 274
275 275 def is_stickerpack(self):
276 276 return self.stickerpack
277 277
278 278 # If tags have parent, add them to the tag list
279 279 @transaction.atomic
280 280 def refresh_tags(self):
281 281 for tag in self.get_tags().all():
282 282 parents = tag.get_all_parents()
283 283 if len(parents) > 0:
284 284 self.tags.add(*parents)
285 285
286 286 def get_reply_tree(self):
287 287 replies = self.get_replies().prefetch_related('refposts')
288 288 tree = []
289 289 for reply in replies:
290 290 parents = reply.refposts.all()
291 291
292 292 found_parent = False
293 293 searching_for_index = False
294 294
295 295 if len(parents) > 0:
296 296 index = 0
297 297 parent_depth = 0
298 298
299 299 indexes_to_insert = []
300 300
301 301 for depth, element in tree:
302 302 index += 1
303 303
304 304 # If this element is next after parent on the same level,
305 305 # insert child before it
306 306 if searching_for_index and depth <= parent_depth:
307 307 indexes_to_insert.append((index - 1, parent_depth))
308 308 searching_for_index = False
309 309
310 310 if element in parents:
311 311 found_parent = True
312 312 searching_for_index = True
313 313 parent_depth = depth
314 314
315 if not found_parent:
316 tree.append((0, reply))
317 else:
318 if searching_for_index:
319 tree.append((parent_depth + 1, reply))
315 if not found_parent:
316 tree.append((0, reply))
317 else:
318 if searching_for_index:
319 tree.append((parent_depth + 1, reply))
320 320
321 offset = 0
322 for last_index, parent_depth in indexes_to_insert:
323 tree.insert(last_index + offset, (parent_depth + 1, reply))
324 offset += 1
321 offset = 0
322 for last_index, parent_depth in indexes_to_insert:
323 tree.insert(last_index + offset, (parent_depth + 1, reply))
324 offset += 1
325 325
326 326 return tree
General Comments 0
You need to be logged in to leave comments. Login now