##// END OF EJS Templates
Added cache for thread opening post
neko259 -
r593:0e05549a default
parent child Browse files
Show More
@@ -1,444 +1,451 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 os
3 import os
4 from random import random
4 from random import random
5 import time
5 import time
6 import math
6 import math
7 import re
7 import re
8 import hashlib
8 import hashlib
9
9
10 from django.core.cache import cache
10 from django.core.cache import cache
11 from django.core.paginator import Paginator
11 from django.core.paginator import Paginator
12 from django.core.urlresolvers import reverse
12 from django.core.urlresolvers import reverse
13
13
14 from django.db import models, transaction
14 from django.db import models, transaction
15 from django.http import Http404
15 from django.http import Http404
16 from django.utils import timezone
16 from django.utils import timezone
17 from markupfield.fields import MarkupField
17 from markupfield.fields import MarkupField
18
18
19 from neboard import settings
19 from neboard import settings
20 from boards import thumbs
20 from boards import thumbs
21
21
22 MAX_TITLE_LENGTH = 50
22 MAX_TITLE_LENGTH = 50
23
23
24 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
25
25
26 CACHE_KEY_PPD = 'ppd'
26 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_POST_URL = 'post_url'
27 CACHE_KEY_POST_URL = 'post_url'
28 CACHE_KEY_OPENING_POST = 'opening_post'
28
29
29 POSTS_PER_DAY_RANGE = range(7)
30 POSTS_PER_DAY_RANGE = range(7)
30
31
31 BAN_REASON_AUTO = 'Auto'
32 BAN_REASON_AUTO = 'Auto'
32
33
33 IMAGE_THUMB_SIZE = (200, 150)
34 IMAGE_THUMB_SIZE = (200, 150)
34
35
35 TITLE_MAX_LENGTH = 50
36 TITLE_MAX_LENGTH = 50
36
37
37 DEFAULT_MARKUP_TYPE = 'markdown'
38 DEFAULT_MARKUP_TYPE = 'markdown'
38
39
39 NO_PARENT = -1
40 NO_PARENT = -1
40 NO_IP = '0.0.0.0'
41 NO_IP = '0.0.0.0'
41 UNKNOWN_UA = ''
42 UNKNOWN_UA = ''
42 ALL_PAGES = -1
43 ALL_PAGES = -1
43 IMAGES_DIRECTORY = 'images/'
44 IMAGES_DIRECTORY = 'images/'
44 FILE_EXTENSION_DELIMITER = '.'
45 FILE_EXTENSION_DELIMITER = '.'
45
46
46 SETTING_MODERATE = "moderate"
47 SETTING_MODERATE = "moderate"
47
48
48 REGEX_REPLY = re.compile('>>(\d+)')
49 REGEX_REPLY = re.compile('>>(\d+)')
49
50
50
51
51 class PostManager(models.Manager):
52 class PostManager(models.Manager):
52
53
53 def create_post(self, title, text, image=None, thread=None,
54 def create_post(self, title, text, image=None, thread=None,
54 ip=NO_IP, tags=None, user=None):
55 ip=NO_IP, tags=None, user=None):
55 """
56 """
56 Create new post
57 Create new post
57 """
58 """
58
59
59 posting_time = timezone.now()
60 posting_time = timezone.now()
60 if not thread:
61 if not thread:
61 thread = Thread.objects.create(bump_time=posting_time,
62 thread = Thread.objects.create(bump_time=posting_time,
62 last_edit_time=posting_time)
63 last_edit_time=posting_time)
63 else:
64 else:
64 thread.bump()
65 thread.bump()
65 thread.last_edit_time = posting_time
66 thread.last_edit_time = posting_time
66 thread.save()
67 thread.save()
67
68
68 post = self.create(title=title,
69 post = self.create(title=title,
69 text=text,
70 text=text,
70 pub_time=posting_time,
71 pub_time=posting_time,
71 thread_new=thread,
72 thread_new=thread,
72 image=image,
73 image=image,
73 poster_ip=ip,
74 poster_ip=ip,
74 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
75 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
75 # last!
76 # last!
76 last_edit_time=posting_time,
77 last_edit_time=posting_time,
77 user=user)
78 user=user)
78
79
79 thread.replies.add(post)
80 thread.replies.add(post)
80 if tags:
81 if tags:
81 linked_tags = []
82 linked_tags = []
82 for tag in tags:
83 for tag in tags:
83 tag_linked_tags = tag.get_linked_tags()
84 tag_linked_tags = tag.get_linked_tags()
84 if len(tag_linked_tags) > 0:
85 if len(tag_linked_tags) > 0:
85 linked_tags.extend(tag_linked_tags)
86 linked_tags.extend(tag_linked_tags)
86
87
87 tags.extend(linked_tags)
88 tags.extend(linked_tags)
88 map(thread.add_tag, tags)
89 map(thread.add_tag, tags)
89
90
90 self._delete_old_threads()
91 self._delete_old_threads()
91 self.connect_replies(post)
92 self.connect_replies(post)
92
93
93 return post
94 return post
94
95
95 def delete_post(self, post):
96 def delete_post(self, post):
96 """
97 """
97 Delete post and update or delete its thread
98 Delete post and update or delete its thread
98 """
99 """
99
100
100 thread = post.thread_new
101 thread = post.thread_new
101
102
102 if post.is_opening():
103 if post.is_opening():
103 thread.delete_with_posts()
104 thread.delete_with_posts()
104 else:
105 else:
105 thread.last_edit_time = timezone.now()
106 thread.last_edit_time = timezone.now()
106 thread.save()
107 thread.save()
107
108
108 post.delete()
109 post.delete()
109
110
110 def delete_posts_by_ip(self, ip):
111 def delete_posts_by_ip(self, ip):
111 """
112 """
112 Delete all posts of the author with same IP
113 Delete all posts of the author with same IP
113 """
114 """
114
115
115 posts = self.filter(poster_ip=ip)
116 posts = self.filter(poster_ip=ip)
116 map(self.delete_post, posts)
117 map(self.delete_post, posts)
117
118
118 # TODO This method may not be needed any more, because django's paginator
119 # TODO This method may not be needed any more, because django's paginator
119 # is used
120 # is used
120 def get_threads(self, tag=None, page=ALL_PAGES,
121 def get_threads(self, tag=None, page=ALL_PAGES,
121 order_by='-bump_time', archived=False):
122 order_by='-bump_time', archived=False):
122 if tag:
123 if tag:
123 threads = tag.threads
124 threads = tag.threads
124
125
125 if not threads.exists():
126 if not threads.exists():
126 raise Http404
127 raise Http404
127 else:
128 else:
128 threads = Thread.objects.all()
129 threads = Thread.objects.all()
129
130
130 threads = threads.filter(archived=archived).order_by(order_by)
131 threads = threads.filter(archived=archived).order_by(order_by)
131
132
132 if page != ALL_PAGES:
133 if page != ALL_PAGES:
133 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
134 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
134 page).object_list
135 page).object_list
135
136
136 return threads
137 return threads
137
138
138 # TODO Move this method to thread manager
139 # TODO Move this method to thread manager
139 def _delete_old_threads(self):
140 def _delete_old_threads(self):
140 """
141 """
141 Preserves maximum thread count. If there are too many threads,
142 Preserves maximum thread count. If there are too many threads,
142 archive the old ones.
143 archive the old ones.
143 """
144 """
144
145
145 threads = self.get_threads()
146 threads = self.get_threads()
146 thread_count = threads.count()
147 thread_count = threads.count()
147
148
148 if thread_count > settings.MAX_THREAD_COUNT:
149 if thread_count > settings.MAX_THREAD_COUNT:
149 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
150 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
150 old_threads = threads[thread_count - num_threads_to_delete:]
151 old_threads = threads[thread_count - num_threads_to_delete:]
151
152
152 for thread in old_threads:
153 for thread in old_threads:
153 thread.archived = True
154 thread.archived = True
154 thread.last_edit_time = timezone.now()
155 thread.last_edit_time = timezone.now()
155 thread.save()
156 thread.save()
156
157
157 def connect_replies(self, post):
158 def connect_replies(self, post):
158 """
159 """
159 Connect replies to a post to show them as a reflink map
160 Connect replies to a post to show them as a reflink map
160 """
161 """
161
162
162 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
163 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
163 post_id = reply_number.group(1)
164 post_id = reply_number.group(1)
164 ref_post = self.filter(id=post_id)
165 ref_post = self.filter(id=post_id)
165 if ref_post.count() > 0:
166 if ref_post.count() > 0:
166 referenced_post = ref_post[0]
167 referenced_post = ref_post[0]
167 referenced_post.referenced_posts.add(post)
168 referenced_post.referenced_posts.add(post)
168 referenced_post.last_edit_time = post.pub_time
169 referenced_post.last_edit_time = post.pub_time
169 referenced_post.save()
170 referenced_post.save()
170
171
171 referenced_thread = referenced_post.thread_new
172 referenced_thread = referenced_post.thread_new
172 referenced_thread.last_edit_time = post.pub_time
173 referenced_thread.last_edit_time = post.pub_time
173 referenced_thread.save()
174 referenced_thread.save()
174
175
175 def get_posts_per_day(self):
176 def get_posts_per_day(self):
176 """
177 """
177 Get average count of posts per day for the last 7 days
178 Get average count of posts per day for the last 7 days
178 """
179 """
179
180
180 today = date.today()
181 today = date.today()
181 ppd = cache.get(CACHE_KEY_PPD + str(today))
182 ppd = cache.get(CACHE_KEY_PPD + str(today))
182 if ppd:
183 if ppd:
183 return ppd
184 return ppd
184
185
185 posts_per_days = []
186 posts_per_days = []
186 for i in POSTS_PER_DAY_RANGE:
187 for i in POSTS_PER_DAY_RANGE:
187 day_end = today - timedelta(i + 1)
188 day_end = today - timedelta(i + 1)
188 day_start = today - timedelta(i + 2)
189 day_start = today - timedelta(i + 2)
189
190
190 day_time_start = timezone.make_aware(datetime.combine(day_start,
191 day_time_start = timezone.make_aware(datetime.combine(day_start,
191 dtime()), timezone.get_current_timezone())
192 dtime()), timezone.get_current_timezone())
192 day_time_end = timezone.make_aware(datetime.combine(day_end,
193 day_time_end = timezone.make_aware(datetime.combine(day_end,
193 dtime()), timezone.get_current_timezone())
194 dtime()), timezone.get_current_timezone())
194
195
195 posts_per_days.append(float(self.filter(
196 posts_per_days.append(float(self.filter(
196 pub_time__lte=day_time_end,
197 pub_time__lte=day_time_end,
197 pub_time__gte=day_time_start).count()))
198 pub_time__gte=day_time_start).count()))
198
199
199 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
200 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
200 len(posts_per_days))
201 len(posts_per_days))
201 cache.set(CACHE_KEY_PPD + str(today), ppd)
202 cache.set(CACHE_KEY_PPD + str(today), ppd)
202 return ppd
203 return ppd
203
204
204
205
205 class Post(models.Model):
206 class Post(models.Model):
206 """A post is a message."""
207 """A post is a message."""
207
208
208 objects = PostManager()
209 objects = PostManager()
209
210
210 class Meta:
211 class Meta:
211 app_label = APP_LABEL_BOARDS
212 app_label = APP_LABEL_BOARDS
212
213
213 # TODO Save original file name to some field
214 # TODO Save original file name to some field
214 def _update_image_filename(self, filename):
215 def _update_image_filename(self, filename):
215 """Get unique image filename"""
216 """Get unique image filename"""
216
217
217 path = IMAGES_DIRECTORY
218 path = IMAGES_DIRECTORY
218 new_name = str(int(time.mktime(time.gmtime())))
219 new_name = str(int(time.mktime(time.gmtime())))
219 new_name += str(int(random() * 1000))
220 new_name += str(int(random() * 1000))
220 new_name += FILE_EXTENSION_DELIMITER
221 new_name += FILE_EXTENSION_DELIMITER
221 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
222 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
222
223
223 return os.path.join(path, new_name)
224 return os.path.join(path, new_name)
224
225
225 title = models.CharField(max_length=TITLE_MAX_LENGTH)
226 title = models.CharField(max_length=TITLE_MAX_LENGTH)
226 pub_time = models.DateTimeField()
227 pub_time = models.DateTimeField()
227 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
228 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
228 escape_html=False)
229 escape_html=False)
229
230
230 image_width = models.IntegerField(default=0)
231 image_width = models.IntegerField(default=0)
231 image_height = models.IntegerField(default=0)
232 image_height = models.IntegerField(default=0)
232
233
233 image_pre_width = models.IntegerField(default=0)
234 image_pre_width = models.IntegerField(default=0)
234 image_pre_height = models.IntegerField(default=0)
235 image_pre_height = models.IntegerField(default=0)
235
236
236 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
237 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
237 blank=True, sizes=(IMAGE_THUMB_SIZE,),
238 blank=True, sizes=(IMAGE_THUMB_SIZE,),
238 width_field='image_width',
239 width_field='image_width',
239 height_field='image_height',
240 height_field='image_height',
240 preview_width_field='image_pre_width',
241 preview_width_field='image_pre_width',
241 preview_height_field='image_pre_height')
242 preview_height_field='image_pre_height')
242 image_hash = models.CharField(max_length=36)
243 image_hash = models.CharField(max_length=36)
243
244
244 poster_ip = models.GenericIPAddressField()
245 poster_ip = models.GenericIPAddressField()
245 poster_user_agent = models.TextField()
246 poster_user_agent = models.TextField()
246
247
247 thread = models.ForeignKey('Post', null=True, default=None)
248 thread = models.ForeignKey('Post', null=True, default=None)
248 thread_new = models.ForeignKey('Thread', null=True, default=None)
249 thread_new = models.ForeignKey('Thread', null=True, default=None)
249 last_edit_time = models.DateTimeField()
250 last_edit_time = models.DateTimeField()
250 user = models.ForeignKey('User', null=True, default=None)
251 user = models.ForeignKey('User', null=True, default=None)
251
252
252 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
253 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
253 null=True,
254 null=True,
254 blank=True, related_name='rfp+')
255 blank=True, related_name='rfp+')
255
256
256 def __unicode__(self):
257 def __unicode__(self):
257 return '#' + str(self.id) + ' ' + self.title + ' (' + \
258 return '#' + str(self.id) + ' ' + self.title + ' (' + \
258 self.text.raw[:50] + ')'
259 self.text.raw[:50] + ')'
259
260
260 def get_title(self):
261 def get_title(self):
261 title = self.title
262 title = self.title
262 if len(title) == 0:
263 if len(title) == 0:
263 title = self.text.rendered
264 title = self.text.rendered
264
265
265 return title
266 return title
266
267
267 def get_sorted_referenced_posts(self):
268 def get_sorted_referenced_posts(self):
268 return self.referenced_posts.order_by('id')
269 return self.referenced_posts.order_by('id')
269
270
270 def is_referenced(self):
271 def is_referenced(self):
271 return self.referenced_posts.all().exists()
272 return self.referenced_posts.all().exists()
272
273
273 def is_opening(self):
274 def is_opening(self):
274 return self.thread_new.get_replies()[0] == self
275 return self.thread_new.get_opening_post() == self
275
276
276 def save(self, *args, **kwargs):
277 def save(self, *args, **kwargs):
277 """
278 """
278 Save the model and compute the image hash
279 Save the model and compute the image hash
279 """
280 """
280
281
281 if not self.pk and self.image:
282 if not self.pk and self.image:
282 md5 = hashlib.md5()
283 md5 = hashlib.md5()
283 for chunk in self.image.chunks():
284 for chunk in self.image.chunks():
284 md5.update(chunk)
285 md5.update(chunk)
285 self.image_hash = md5.hexdigest()
286 self.image_hash = md5.hexdigest()
286 super(Post, self).save(*args, **kwargs)
287 super(Post, self).save(*args, **kwargs)
287
288
288 @transaction.atomic
289 @transaction.atomic
289 def add_tag(self, tag):
290 def add_tag(self, tag):
290 edit_time = timezone.now()
291 edit_time = timezone.now()
291
292
292 thread = self.thread_new
293 thread = self.thread_new
293 thread.add_tag(tag)
294 thread.add_tag(tag)
294 self.last_edit_time = edit_time
295 self.last_edit_time = edit_time
295 self.save()
296 self.save()
296
297
297 thread.last_edit_time = edit_time
298 thread.last_edit_time = edit_time
298 thread.save()
299 thread.save()
299
300
300 @transaction.atomic
301 @transaction.atomic
301 def remove_tag(self, tag):
302 def remove_tag(self, tag):
302 edit_time = timezone.now()
303 edit_time = timezone.now()
303
304
304 thread = self.thread_new
305 thread = self.thread_new
305 thread.remove_tag(tag)
306 thread.remove_tag(tag)
306 self.last_edit_time = edit_time
307 self.last_edit_time = edit_time
307 self.save()
308 self.save()
308
309
309 thread.last_edit_time = edit_time
310 thread.last_edit_time = edit_time
310 thread.save()
311 thread.save()
311
312
312 def get_url(self):
313 def get_url(self):
313 """
314 """
314 Get full url to this post
315 Get full url to this post
315 """
316 """
316
317
317 cache_key = CACHE_KEY_POST_URL + str(self.id)
318 cache_key = CACHE_KEY_POST_URL + str(self.id)
318 link = cache.get(cache_key)
319 link = cache.get(cache_key)
319
320
320 if not link:
321 if not link:
321 opening_post = self.thread_new.get_opening_post()
322 opening_post = self.thread_new.get_opening_post()
322 if self != opening_post:
323 if self != opening_post:
323 link = reverse('thread',
324 link = reverse('thread',
324 kwargs={'post_id': opening_post.id}) + '#' + str(
325 kwargs={'post_id': opening_post.id}) + '#' + str(
325 self.id)
326 self.id)
326 else:
327 else:
327 link = reverse('thread', kwargs={'post_id': self.id})
328 link = reverse('thread', kwargs={'post_id': self.id})
328
329
329 cache.set(cache_key, link)
330 cache.set(cache_key, link)
330
331
331 return link
332 return link
332
333
333
334
334 class Thread(models.Model):
335 class Thread(models.Model):
335
336
336 class Meta:
337 class Meta:
337 app_label = APP_LABEL_BOARDS
338 app_label = APP_LABEL_BOARDS
338
339
339 tags = models.ManyToManyField('Tag')
340 tags = models.ManyToManyField('Tag')
340 bump_time = models.DateTimeField()
341 bump_time = models.DateTimeField()
341 last_edit_time = models.DateTimeField()
342 last_edit_time = models.DateTimeField()
342 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
343 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
343 blank=True, related_name='tre+')
344 blank=True, related_name='tre+')
344 archived = models.BooleanField(default=False)
345 archived = models.BooleanField(default=False)
345
346
346 def get_tags(self):
347 def get_tags(self):
347 """
348 """
348 Get a sorted tag list
349 Get a sorted tag list
349 """
350 """
350
351
351 return self.tags.order_by('name')
352 return self.tags.order_by('name')
352
353
353 def bump(self):
354 def bump(self):
354 """
355 """
355 Bump (move to up) thread
356 Bump (move to up) thread
356 """
357 """
357
358
358 if self.can_bump():
359 if self.can_bump():
359 self.bump_time = timezone.now()
360 self.bump_time = timezone.now()
360
361
361 def get_reply_count(self):
362 def get_reply_count(self):
362 return self.replies.count()
363 return self.replies.count()
363
364
364 def get_images_count(self):
365 def get_images_count(self):
365 return self.replies.filter(image_width__gt=0).count()
366 return self.replies.filter(image_width__gt=0).count()
366
367
367 def can_bump(self):
368 def can_bump(self):
368 """
369 """
369 Check if the thread can be bumped by replying
370 Check if the thread can be bumped by replying
370 """
371 """
371
372
372 if self.archived:
373 if self.archived:
373 return False
374 return False
374
375
375 post_count = self.get_reply_count()
376 post_count = self.get_reply_count()
376
377
377 return post_count < settings.MAX_POSTS_PER_THREAD
378 return post_count < settings.MAX_POSTS_PER_THREAD
378
379
379 def delete_with_posts(self):
380 def delete_with_posts(self):
380 """
381 """
381 Completely delete thread and all its posts
382 Completely delete thread and all its posts
382 """
383 """
383
384
384 if self.replies.count() > 0:
385 if self.replies.count() > 0:
385 self.replies.all().delete()
386 self.replies.all().delete()
386
387
387 self.delete()
388 self.delete()
388
389
389 def get_last_replies(self):
390 def get_last_replies(self):
390 """
391 """
391 Get last replies, not including opening post
392 Get last replies, not including opening post
392 """
393 """
393
394
394 if settings.LAST_REPLIES_COUNT > 0:
395 if settings.LAST_REPLIES_COUNT > 0:
395 reply_count = self.get_reply_count()
396 reply_count = self.get_reply_count()
396
397
397 if reply_count > 0:
398 if reply_count > 0:
398 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
399 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
399 reply_count - 1)
400 reply_count - 1)
400 last_replies = self.replies.all().order_by('pub_time')[
401 last_replies = self.replies.all().order_by('pub_time')[
401 reply_count - reply_count_to_show:]
402 reply_count - reply_count_to_show:]
402
403
403 return last_replies
404 return last_replies
404
405
405 def get_skipped_replies_count(self):
406 def get_skipped_replies_count(self):
406 last_replies = self.get_last_replies()
407 last_replies = self.get_last_replies()
407 return self.get_reply_count() - len(last_replies) - 1
408 return self.get_reply_count() - len(last_replies) - 1
408
409
409 def get_replies(self):
410 def get_replies(self):
410 """
411 """
411 Get sorted thread posts
412 Get sorted thread posts
412 """
413 """
413
414
414 return self.replies.all().order_by('pub_time')
415 return self.replies.all().order_by('pub_time')
415
416
416 def add_tag(self, tag):
417 def add_tag(self, tag):
417 """
418 """
418 Connect thread to a tag and tag to a thread
419 Connect thread to a tag and tag to a thread
419 """
420 """
420
421
421 self.tags.add(tag)
422 self.tags.add(tag)
422 tag.threads.add(self)
423 tag.threads.add(self)
423
424
424 def remove_tag(self, tag):
425 def remove_tag(self, tag):
425 self.tags.remove(tag)
426 self.tags.remove(tag)
426 tag.threads.remove(self)
427 tag.threads.remove(self)
427
428
428 def get_opening_post(self):
429 def get_opening_post(self):
429 """
430 """
430 Get first post of the thread
431 Get first post of the thread
431 """
432 """
432
433
433 return self.get_replies()[0]
434 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
435 opening_post = cache.get(cache_key)
436 if not opening_post:
437 opening_post = self.get_replies()[0]
438 cache.set(cache_key, opening_post)
439
440 return opening_post
434
441
435 def __unicode__(self):
442 def __unicode__(self):
436 return str(self.id)
443 return str(self.id)
437
444
438 def get_pub_time(self):
445 def get_pub_time(self):
439 """
446 """
440 Thread does not have its own pub time, so we need to get it from
447 Thread does not have its own pub time, so we need to get it from
441 the opening post
448 the opening post
442 """
449 """
443
450
444 return self.get_opening_post().pub_time
451 return self.get_opening_post().pub_time
General Comments 0
You need to be logged in to leave comments. Login now