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