##// END OF EJS Templates
Save "opening post" flag with the post itself and don't count it every time. Speed up getting posts with attachments and images
neko259 -
r1337:4c8c3ec5 default
parent child Browse files
Show More
@@ -0,0 +1,20 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0025_auto_20150825_2049'),
11 ]
12
13 operations = [
14 migrations.AddField(
15 model_name='post',
16 name='opening',
17 field=models.BooleanField(default=False),
18 preserve_default=False,
19 ),
20 ]
@@ -0,0 +1,22 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations
5
6
7 class Migration(migrations.Migration):
8
9 def build_opening_flag(apps, schema_editor):
10 Post = apps.get_model('boards', 'Post')
11 for post in Post.objects.all():
12 op = Post.objects.filter(threads__in=[post.thread]).order_by('pub_time').first()
13 post.opening = op.id == post.id
14 post.save(update_fields=['opening'])
15
16 dependencies = [
17 ('boards', '0026_post_opening'),
18 ]
19
20 operations = [
21 migrations.RunPython(build_opening_flag),
22 ]
@@ -1,455 +1,450 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 import uuid
6 6
7 7 from django.core.exceptions import ObjectDoesNotExist
8 8 from django.core.urlresolvers import reverse
9 9 from django.db import models, transaction
10 10 from django.db.models import TextField, QuerySet
11 11 from django.template.loader import render_to_string
12 12 from django.utils import timezone
13 13
14 14 from boards import settings
15 15 from boards.abstracts.tripcode import Tripcode
16 16 from boards.mdx_neboard import Parser
17 17 from boards.models import PostImage, Attachment
18 18 from boards.models.base import Viewable
19 19 from boards import utils
20 20 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
21 21 from boards.models.user import Notification, Ban
22 22 import boards.models.thread
23
23 from boards.utils import cached_result
24 24
25 25 APP_LABEL_BOARDS = 'boards'
26 26
27 27 POSTS_PER_DAY_RANGE = 7
28 28
29 29 BAN_REASON_AUTO = 'Auto'
30 30
31 31 IMAGE_THUMB_SIZE = (200, 150)
32 32
33 33 TITLE_MAX_LENGTH = 200
34 34
35 35 # TODO This should be removed
36 36 NO_IP = '0.0.0.0'
37 37
38 38 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
39 39 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
40 40
41 41 PARAMETER_TRUNCATED = 'truncated'
42 42 PARAMETER_TAG = 'tag'
43 43 PARAMETER_OFFSET = 'offset'
44 44 PARAMETER_DIFF_TYPE = 'type'
45 45 PARAMETER_CSS_CLASS = 'css_class'
46 46 PARAMETER_THREAD = 'thread'
47 47 PARAMETER_IS_OPENING = 'is_opening'
48 48 PARAMETER_MODERATOR = 'moderator'
49 49 PARAMETER_POST = 'post'
50 50 PARAMETER_OP_ID = 'opening_post_id'
51 51 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
52 52 PARAMETER_REPLY_LINK = 'reply_link'
53 53 PARAMETER_NEED_OP_DATA = 'need_op_data'
54 54
55 55 POST_VIEW_PARAMS = (
56 56 'need_op_data',
57 57 'reply_link',
58 58 'moderator',
59 59 'need_open_link',
60 60 'truncated',
61 61 'mode_tree',
62 62 )
63 63
64 64 IMAGE_TYPES = (
65 65 'jpeg',
66 66 'jpg',
67 67 'png',
68 68 'bmp',
69 69 'gif',
70 70 )
71 71
72 72
73 73 class PostManager(models.Manager):
74 74 @transaction.atomic
75 75 def create_post(self, title: str, text: str, file=None, thread=None,
76 76 ip=NO_IP, tags: list=None, opening_posts: list=None, tripcode=None):
77 77 """
78 78 Creates new post
79 79 """
80 80
81 81 is_banned = Ban.objects.filter(ip=ip).exists()
82 82
83 83 # TODO Raise specific exception and catch it in the views
84 84 if is_banned:
85 85 raise Exception("This user is banned")
86 86
87 87 if not tags:
88 88 tags = []
89 89 if not opening_posts:
90 90 opening_posts = []
91 91
92 92 posting_time = timezone.now()
93 93 new_thread = False
94 94 if not thread:
95 95 thread = boards.models.thread.Thread.objects.create(
96 96 bump_time=posting_time, last_edit_time=posting_time)
97 97 list(map(thread.tags.add, tags))
98 98 boards.models.thread.Thread.objects.process_oldest_threads()
99 99 new_thread = True
100 100
101 101 pre_text = Parser().preparse(text)
102 102
103 103 post = self.create(title=title,
104 104 text=pre_text,
105 105 pub_time=posting_time,
106 106 poster_ip=ip,
107 107 thread=thread,
108 108 last_edit_time=posting_time,
109 tripcode=tripcode)
109 tripcode=tripcode,
110 opening=new_thread)
110 111 post.threads.add(thread)
111 112
112 113 logger = logging.getLogger('boards.post.create')
113 114
114 115 logger.info('Created post {} by {}'.format(post, post.poster_ip))
115 116
116 117 # TODO Move this to other place
117 118 if file:
118 119 file_type = file.name.split('.')[-1].lower()
119 120 if file_type in IMAGE_TYPES:
120 121 post.images.add(PostImage.objects.create_with_hash(file))
121 122 else:
122 123 post.attachments.add(Attachment.objects.create_with_hash(file))
123 124
124 125 post.build_url()
125 126 post.connect_replies()
126 127 post.connect_threads(opening_posts)
127 128 post.connect_notifications()
128 129
129 130 # Thread needs to be bumped only when the post is already created
130 131 if not new_thread:
131 132 thread.last_edit_time = posting_time
132 133 thread.bump()
133 134 thread.save()
134 135
135 136 return post
136 137
137 138 def delete_posts_by_ip(self, ip):
138 139 """
139 140 Deletes all posts of the author with same IP
140 141 """
141 142
142 143 posts = self.filter(poster_ip=ip)
143 144 for post in posts:
144 145 post.delete()
145 146
146 147 @utils.cached_result()
147 148 def get_posts_per_day(self) -> float:
148 149 """
149 150 Gets average count of posts per day for the last 7 days
150 151 """
151 152
152 153 day_end = date.today()
153 154 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
154 155
155 156 day_time_start = timezone.make_aware(datetime.combine(
156 157 day_start, dtime()), timezone.get_current_timezone())
157 158 day_time_end = timezone.make_aware(datetime.combine(
158 159 day_end, dtime()), timezone.get_current_timezone())
159 160
160 161 posts_per_period = float(self.filter(
161 162 pub_time__lte=day_time_end,
162 163 pub_time__gte=day_time_start).count())
163 164
164 165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
165 166
166 167 return ppd
167 168
168 169
169 170 class Post(models.Model, Viewable):
170 171 """A post is a message."""
171 172
172 173 objects = PostManager()
173 174
174 175 class Meta:
175 176 app_label = APP_LABEL_BOARDS
176 177 ordering = ('id',)
177 178
178 179 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
179 180 pub_time = models.DateTimeField()
180 181 text = TextField(blank=True, null=True)
181 182 _text_rendered = TextField(blank=True, null=True, editable=False)
182 183
183 184 images = models.ManyToManyField(PostImage, null=True, blank=True,
184 185 related_name='post_images', db_index=True)
185 186 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
186 187 related_name='attachment_posts')
187 188
188 189 poster_ip = models.GenericIPAddressField()
189 190
190 191 # TODO This field can be removed cause UID is used for update now
191 192 last_edit_time = models.DateTimeField()
192 193
193 194 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
194 195 null=True,
195 196 blank=True, related_name='refposts',
196 197 db_index=True)
197 198 refmap = models.TextField(null=True, blank=True)
198 199 threads = models.ManyToManyField('Thread', db_index=True)
199 200 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
200 201
201 202 url = models.TextField()
202 203 uid = models.TextField(db_index=True)
203 204
204 205 tripcode = models.CharField(max_length=50, null=True)
206 opening = models.BooleanField()
205 207
206 208 def __str__(self):
207 209 return 'P#{}/{}'.format(self.id, self.title)
208 210
209 211 def get_referenced_posts(self):
210 212 threads = self.get_threads().all()
211 213 return self.referenced_posts.filter(threads__in=threads)\
212 214 .order_by('pub_time').distinct().all()
213 215
214 216 def get_title(self) -> str:
215 217 """
216 218 Gets original post title or part of its text.
217 219 """
218 220
219 221 title = self.title
220 222 if not title:
221 223 title = self.get_text()
222 224
223 225 return title
224 226
225 227 def build_refmap(self) -> None:
226 228 """
227 229 Builds a replies map string from replies list. This is a cache to stop
228 230 the server from recalculating the map on every post show.
229 231 """
230 232
231 233 post_urls = [refpost.get_link_view()
232 234 for refpost in self.referenced_posts.all()]
233 235
234 236 self.refmap = ', '.join(post_urls)
235 237
236 238 def is_referenced(self) -> bool:
237 239 return self.refmap and len(self.refmap) > 0
238 240
239 241 def is_opening(self) -> bool:
240 242 """
241 243 Checks if this is an opening post or just a reply.
242 244 """
243 245
244 return self.get_thread().get_opening_post_id() == self.id
246 return self.opening
245 247
246 248 def get_absolute_url(self):
247 249 if self.url:
248 250 return self.url
249 251 else:
250 252 opening_id = self.get_thread().get_opening_post_id()
251 253 post_url = reverse('thread', kwargs={'post_id': opening_id})
252 254 if self.id != opening_id:
253 255 post_url += '#' + str(self.id)
254 256 return post_url
255 257
256 258 def get_thread(self):
257 259 return self.thread
258 260
259 261 def get_threads(self) -> QuerySet:
260 262 """
261 263 Gets post's thread.
262 264 """
263 265
264 266 return self.threads
265 267
266 268 def get_view(self, *args, **kwargs) -> str:
267 269 """
268 270 Renders post's HTML view. Some of the post params can be passed over
269 271 kwargs for the means of caching (if we view the thread, some params
270 272 are same for every post and don't need to be computed over and over.
271 273 """
272 274
273 275 thread = self.get_thread()
274 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
275
276 if is_opening:
277 opening_post_id = self.id
278 else:
279 opening_post_id = thread.get_opening_post_id()
280 276
281 277 css_class = 'post'
282 278 if thread.archived:
283 279 css_class += ' archive_post'
284 280 elif not thread.can_bump():
285 281 css_class += ' dead_post'
286 282
287 283 params = dict()
288 284 for param in POST_VIEW_PARAMS:
289 285 if param in kwargs:
290 286 params[param] = kwargs[param]
291 287
292 288 params.update({
293 289 PARAMETER_POST: self,
294 PARAMETER_IS_OPENING: is_opening,
290 PARAMETER_IS_OPENING: self.is_opening(),
295 291 PARAMETER_THREAD: thread,
296 292 PARAMETER_CSS_CLASS: css_class,
297 PARAMETER_OP_ID: opening_post_id,
298 293 })
299 294
300 295 return render_to_string('boards/post.html', params)
301 296
302 297 def get_search_view(self, *args, **kwargs):
303 298 return self.get_view(need_op_data=True, *args, **kwargs)
304 299
305 300 def get_first_image(self) -> PostImage:
306 301 return self.images.earliest('id')
307 302
308 303 def delete(self, using=None):
309 304 """
310 305 Deletes all post images and the post itself.
311 306 """
312 307
313 308 for image in self.images.all():
314 309 image_refs_count = image.post_images.count()
315 310 if image_refs_count == 1:
316 311 image.delete()
317 312
318 313 for attachment in self.attachments.all():
319 314 attachment_refs_count = attachment.attachment_posts.count()
320 315 if attachment_refs_count == 1:
321 316 attachment.delete()
322 317
323 318 thread = self.get_thread()
324 319 thread.last_edit_time = timezone.now()
325 320 thread.save()
326 321
327 322 super(Post, self).delete(using)
328 323
329 324 logging.getLogger('boards.post.delete').info(
330 325 'Deleted post {}'.format(self))
331 326
332 327 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
333 328 include_last_update=False) -> str:
334 329 """
335 330 Gets post HTML or JSON data that can be rendered on a page or used by
336 331 API.
337 332 """
338 333
339 334 return get_exporter(format_type).export(self, request,
340 335 include_last_update)
341 336
342 337 def notify_clients(self, recursive=True):
343 338 """
344 339 Sends post HTML data to the thread web socket.
345 340 """
346 341
347 342 if not settings.get_bool('External', 'WebsocketsEnabled'):
348 343 return
349 344
350 345 thread_ids = list()
351 346 for thread in self.get_threads().all():
352 347 thread_ids.append(thread.id)
353 348
354 349 thread.notify_clients()
355 350
356 351 if recursive:
357 352 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
358 353 post_id = reply_number.group(1)
359 354
360 355 try:
361 356 ref_post = Post.objects.get(id=post_id)
362 357
363 358 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
364 359 # If post is in this thread, its thread was already notified.
365 360 # Otherwise, notify its thread separately.
366 361 ref_post.notify_clients(recursive=False)
367 362 except ObjectDoesNotExist:
368 363 pass
369 364
370 365 def build_url(self):
371 366 self.url = self.get_absolute_url()
372 367 self.save(update_fields=['url'])
373 368
374 369 def save(self, force_insert=False, force_update=False, using=None,
375 370 update_fields=None):
376 371 self._text_rendered = Parser().parse(self.get_raw_text())
377 372
378 373 self.uid = str(uuid.uuid4())
379 374 if update_fields is not None and 'uid' not in update_fields:
380 375 update_fields += ['uid']
381 376
382 377 if self.id:
383 378 for thread in self.get_threads().all():
384 379 thread.last_edit_time = self.last_edit_time
385 380
386 381 thread.save(update_fields=['last_edit_time', 'bumpable'])
387 382
388 383 super().save(force_insert, force_update, using, update_fields)
389 384
390 385 def get_text(self) -> str:
391 386 return self._text_rendered
392 387
393 388 def get_raw_text(self) -> str:
394 389 return self.text
395 390
396 391 def get_absolute_id(self) -> str:
397 392 """
398 393 If the post has many threads, shows its main thread OP id in the post
399 394 ID.
400 395 """
401 396
402 397 if self.get_threads().count() > 1:
403 398 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
404 399 else:
405 400 return str(self.id)
406 401
407 402 def connect_notifications(self):
408 403 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
409 404 user_name = reply_number.group(1).lower()
410 405 Notification.objects.get_or_create(name=user_name, post=self)
411 406
412 407 def connect_replies(self):
413 408 """
414 409 Connects replies to a post to show them as a reflink map
415 410 """
416 411
417 412 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
418 413 post_id = reply_number.group(1)
419 414
420 415 try:
421 416 referenced_post = Post.objects.get(id=post_id)
422 417
423 418 referenced_post.referenced_posts.add(self)
424 419 referenced_post.last_edit_time = self.pub_time
425 420 referenced_post.build_refmap()
426 421 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
427 422 except ObjectDoesNotExist:
428 423 pass
429 424
430 425 def connect_threads(self, opening_posts):
431 426 for opening_post in opening_posts:
432 427 threads = opening_post.get_threads().all()
433 428 for thread in threads:
434 429 if thread.can_bump():
435 430 thread.update_bump_status()
436 431
437 432 thread.last_edit_time = self.last_edit_time
438 433 thread.save(update_fields=['last_edit_time', 'bumpable'])
439 434 self.threads.add(opening_post.get_thread())
440 435
441 436 def get_tripcode(self):
442 437 if self.tripcode:
443 438 return Tripcode(self.tripcode)
444 439
445 440 def get_link_view(self):
446 441 """
447 442 Gets view of a reflink to the post.
448 443 """
449 444
450 445 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
451 446 self.id)
452 447 if self.is_opening():
453 448 result = '<b>{}</b>'.format(result)
454 449
455 450 return result
@@ -1,234 +1,235 b''
1 1 import logging
2 2 from adjacent import Client
3 3
4 4 from django.db.models import Count, Sum, QuerySet
5 5 from django.utils import timezone
6 6 from django.db import models
7 7
8 8 from boards import settings
9 9 import boards
10 10 from boards.utils import cached_result, datetime_to_epoch
11 11 from boards.models.post import Post
12 12 from boards.models.tag import Tag
13 13
14 14
15 15 __author__ = 'neko259'
16 16
17 17
18 18 logger = logging.getLogger(__name__)
19 19
20 20
21 21 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 22 WS_NOTIFICATION_TYPE = 'notification_type'
23 23
24 24 WS_CHANNEL_THREAD = "thread:"
25 25
26 26
27 27 class ThreadManager(models.Manager):
28 28 def process_oldest_threads(self):
29 29 """
30 30 Preserves maximum thread count. If there are too many threads,
31 31 archive or delete the old ones.
32 32 """
33 33
34 34 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
35 35 thread_count = threads.count()
36 36
37 37 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
38 38 if thread_count > max_thread_count:
39 39 num_threads_to_delete = thread_count - max_thread_count
40 40 old_threads = threads[thread_count - num_threads_to_delete:]
41 41
42 42 for thread in old_threads:
43 43 if settings.get_bool('Storage', 'ArchiveThreads'):
44 44 self._archive_thread(thread)
45 45 else:
46 46 thread.delete()
47 47
48 48 logger.info('Processed %d old threads' % num_threads_to_delete)
49 49
50 50 def _archive_thread(self, thread):
51 51 thread.archived = True
52 52 thread.bumpable = False
53 53 thread.last_edit_time = timezone.now()
54 54 thread.update_posts_time()
55 55 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
56 56
57 57
58 58 def get_thread_max_posts():
59 59 return settings.get_int('Messages', 'MaxPostsPerThread')
60 60
61 61
62 62 class Thread(models.Model):
63 63 objects = ThreadManager()
64 64
65 65 class Meta:
66 66 app_label = 'boards'
67 67
68 68 tags = models.ManyToManyField('Tag', related_name='thread_tags')
69 69 bump_time = models.DateTimeField(db_index=True)
70 70 last_edit_time = models.DateTimeField()
71 71 archived = models.BooleanField(default=False)
72 72 bumpable = models.BooleanField(default=True)
73 73 max_posts = models.IntegerField(default=get_thread_max_posts)
74 74
75 75 def get_tags(self) -> QuerySet:
76 76 """
77 77 Gets a sorted tag list.
78 78 """
79 79
80 80 return self.tags.order_by('name')
81 81
82 82 def bump(self):
83 83 """
84 84 Bumps (moves to up) thread if possible.
85 85 """
86 86
87 87 if self.can_bump():
88 88 self.bump_time = self.last_edit_time
89 89
90 90 self.update_bump_status()
91 91
92 92 logger.info('Bumped thread %d' % self.id)
93 93
94 94 def has_post_limit(self) -> bool:
95 95 return self.max_posts > 0
96 96
97 97 def update_bump_status(self, exclude_posts=None):
98 98 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
99 99 self.bumpable = False
100 100 self.update_posts_time(exclude_posts=exclude_posts)
101 101
102 102 def _get_cache_key(self):
103 103 return [datetime_to_epoch(self.last_edit_time)]
104 104
105 105 @cached_result(key_method=_get_cache_key)
106 106 def get_reply_count(self) -> int:
107 107 return self.get_replies().count()
108 108
109 109 @cached_result(key_method=_get_cache_key)
110 110 def get_images_count(self) -> int:
111 111 return self.get_replies().annotate(images_count=Count(
112 112 'images')).aggregate(Sum('images_count'))['images_count__sum']
113 113
114 114 def can_bump(self) -> bool:
115 115 """
116 116 Checks if the thread can be bumped by replying to it.
117 117 """
118 118
119 119 return self.bumpable and not self.archived
120 120
121 121 def get_last_replies(self) -> QuerySet:
122 122 """
123 123 Gets several last replies, not including opening post
124 124 """
125 125
126 126 last_replies_count = settings.get_int('View', 'LastRepliesCount')
127 127
128 128 if last_replies_count > 0:
129 129 reply_count = self.get_reply_count()
130 130
131 131 if reply_count > 0:
132 132 reply_count_to_show = min(last_replies_count,
133 133 reply_count - 1)
134 134 replies = self.get_replies()
135 135 last_replies = replies[reply_count - reply_count_to_show:]
136 136
137 137 return last_replies
138 138
139 139 def get_skipped_replies_count(self) -> int:
140 140 """
141 141 Gets number of posts between opening post and last replies.
142 142 """
143 143 reply_count = self.get_reply_count()
144 144 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
145 145 reply_count - 1)
146 146 return reply_count - last_replies_count - 1
147 147
148 148 def get_replies(self, view_fields_only=False) -> QuerySet:
149 149 """
150 150 Gets sorted thread posts
151 151 """
152 152
153 153 query = Post.objects.filter(threads__in=[self])
154 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
154 query = query.order_by('pub_time').prefetch_related(
155 'images', 'thread', 'threads', 'attachments')
155 156 if view_fields_only:
156 157 query = query.defer('poster_ip')
157 158 return query.all()
158 159
159 160 def get_top_level_replies(self) -> QuerySet:
160 161 return self.get_replies().exclude(refposts__threads__in=[self])
161 162
162 163 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
163 164 """
164 165 Gets replies that have at least one image attached
165 166 """
166 167
167 168 return self.get_replies(view_fields_only).annotate(images_count=Count(
168 169 'images')).filter(images_count__gt=0)
169 170
170 171 def get_opening_post(self, only_id=False) -> Post:
171 172 """
172 173 Gets the first post of the thread
173 174 """
174 175
175 176 query = self.get_replies().order_by('pub_time')
176 177 if only_id:
177 178 query = query.only('id')
178 179 opening_post = query.first()
179 180
180 181 return opening_post
181 182
182 183 @cached_result()
183 184 def get_opening_post_id(self) -> int:
184 185 """
185 186 Gets ID of the first thread post.
186 187 """
187 188
188 189 return self.get_opening_post(only_id=True).id
189 190
190 191 def get_pub_time(self):
191 192 """
192 193 Gets opening post's pub time because thread does not have its own one.
193 194 """
194 195
195 196 return self.get_opening_post().pub_time
196 197
197 198 def __str__(self):
198 199 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
199 200
200 201 def get_tag_url_list(self) -> list:
201 202 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
202 203
203 204 def update_posts_time(self, exclude_posts=None):
204 205 last_edit_time = self.last_edit_time
205 206
206 207 for post in self.post_set.all():
207 208 if exclude_posts is None or post not in exclude_posts:
208 209 # Manual update is required because uids are generated on save
209 210 post.last_edit_time = last_edit_time
210 211 post.save(update_fields=['last_edit_time'])
211 212
212 213 post.get_threads().update(last_edit_time=last_edit_time)
213 214
214 215 def notify_clients(self):
215 216 if not settings.get_bool('External', 'WebsocketsEnabled'):
216 217 return
217 218
218 219 client = Client()
219 220
220 221 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
221 222 client.publish(channel_name, {
222 223 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
223 224 })
224 225 client.send()
225 226
226 227 def get_absolute_url(self):
227 228 return self.get_opening_post().get_absolute_url()
228 229
229 230 def get_required_tags(self):
230 231 return self.get_tags().filter(required=True)
231 232
232 233 def get_replies_newer(self, post_id):
233 234 return self.get_replies().filter(id__gt=post_id)
234 235
@@ -1,186 +1,186 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load board %}
5 5 {% load static %}
6 6 {% load tz %}
7 7
8 8 {% block head %}
9 9 <meta name="robots" content="noindex">
10 10
11 11 {% if tag %}
12 12 <title>{{ tag.name }} - {{ site_name }}</title>
13 13 {% else %}
14 14 <title>{{ site_name }}</title>
15 15 {% endif %}
16 16
17 17 {% if prev_page_link %}
18 18 <link rel="prev" href="{{ prev_page_link }}" />
19 19 {% endif %}
20 20 {% if next_page_link %}
21 21 <link rel="next" href="{{ next_page_link }}" />
22 22 {% endif %}
23 23
24 24 {% endblock %}
25 25
26 26 {% block content %}
27 27
28 28 {% get_current_language as LANGUAGE_CODE %}
29 29 {% get_current_timezone as TIME_ZONE %}
30 30
31 31 {% for banner in banners %}
32 32 <div class="post">
33 33 <div class="title">{{ banner.title }}</div>
34 34 <div>{{ banner.text }}</div>
35 35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 36 </div>
37 37 {% endfor %}
38 38
39 39 {% if tag %}
40 40 <div class="tag_info">
41 41 {% if random_image_post %}
42 42 <div class="tag-image">
43 43 {% with image=random_image_post.images.first %}
44 44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 45 src="{{ image.image.url_200x150 }}"
46 46 width="{{ image.pre_width }}"
47 47 height="{{ image.pre_height }}"/></a>
48 48 {% endwith %}
49 49 </div>
50 50 {% endif %}
51 51 <div class="tag-text-data">
52 52 <h2>
53 53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
54 54 {% if is_favorite %}
55 55 <button name="method" value="unsubscribe" class="fav">β˜…</button>
56 56 {% else %}
57 57 <button name="method" value="subscribe" class="not_fav">β˜…</button>
58 58 {% endif %}
59 59 </form>
60 60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
61 61 {% if is_hidden %}
62 62 <button name="method" value="unhide" class="fav">H</button>
63 63 {% else %}
64 64 <button name="method" value="hide" class="not_fav">H</button>
65 65 {% endif %}
66 66 </form>
67 67 {{ tag.get_view|safe }}
68 68 {% if moderator %}
69 69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
70 70 {% endif %}
71 71 </h2>
72 72 {% if tag.get_description %}
73 73 <p>{{ tag.get_description|safe }}</p>
74 74 {% endif %}
75 75 <p>{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}</p>
76 76 {% if related_tags %}
77 77 <p>{% trans 'Related tags:' %}
78 78 {% for rel_tag in related_tags %}
79 79 {{ rel_tag.get_view|safe }}{% if not forloop.last %}, {% else %}.{% endif %}
80 80 {% endfor %}
81 81 </p>
82 82 {% endif %}
83 83 </div>
84 84 </div>
85 85 {% endif %}
86 86
87 87 {% if threads %}
88 88 {% if prev_page_link %}
89 89 <div class="page_link">
90 90 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
91 91 </div>
92 92 {% endif %}
93 93
94 94 {% for thread in threads %}
95 95 <div class="thread">
96 {% post_view thread.get_opening_post moderator=moderator is_opening=True thread=thread truncated=True need_open_link=True %}
96 {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %}
97 97 {% if not thread.archived %}
98 98 {% with last_replies=thread.get_last_replies %}
99 99 {% if last_replies %}
100 100 {% with skipped_replies_count=thread.get_skipped_replies_count %}
101 101 {% if skipped_replies_count %}
102 102 <div class="skipped_replies">
103 103 <a href="{% url 'thread' thread.get_opening_post_id %}">
104 104 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
105 105 </a>
106 106 </div>
107 107 {% endif %}
108 108 {% endwith %}
109 109 <div class="last-replies">
110 110 {% for post in last_replies %}
111 {% post_view post is_opening=False moderator=moderator truncated=True %}
111 {% post_view post moderator=moderator truncated=True %}
112 112 {% endfor %}
113 113 </div>
114 114 {% endif %}
115 115 {% endwith %}
116 116 {% endif %}
117 117 </div>
118 118 {% endfor %}
119 119
120 120 {% if next_page_link %}
121 121 <div class="page_link">
122 122 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
123 123 </div>
124 124 {% endif %}
125 125 {% else %}
126 126 <div class="post">
127 127 {% trans 'No threads exist. Create the first one!' %}</div>
128 128 {% endif %}
129 129
130 130 <div class="post-form-w">
131 131 <script src="{% static 'js/panel.js' %}"></script>
132 132 <div class="post-form">
133 133 <div class="form-title">{% trans "Create new thread" %}</div>
134 134 <div class="swappable-form-full">
135 135 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
136 136 {{ form.as_div }}
137 137 <div class="form-submit">
138 138 <input type="submit" value="{% trans "Post" %}"/>
139 139 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
140 140 </div>
141 141 </form>
142 142 </div>
143 143 <div>
144 144 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
145 145 </div>
146 146 <div id="preview-text"></div>
147 147 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
148 148 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
149 149 </div>
150 150 </div>
151 151
152 152 <script src="{% static 'js/form.js' %}"></script>
153 153 <script src="{% static 'js/thread_create.js' %}"></script>
154 154
155 155 {% endblock %}
156 156
157 157 {% block metapanel %}
158 158
159 159 <span class="metapanel">
160 160 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
161 161 {% trans "Pages:" %}
162 162 [
163 163 {% with dividers=paginator.get_dividers %}
164 164 {% for page in paginator.get_divided_range %}
165 165 {% if page in dividers %}
166 166 …,
167 167 {% endif %}
168 168 <a
169 169 {% ifequal page current_page.number %}
170 170 class="current_page"
171 171 {% endifequal %}
172 172 href="
173 173 {% if tag %}
174 174 {% url "tag" tag_name=tag.name %}?page={{ page }}
175 175 {% else %}
176 176 {% url "index" %}?page={{ page }}
177 177 {% endif %}
178 178 ">{{ page }}</a>
179 179 {% if not forloop.last %},{% endif %}
180 180 {% endfor %}
181 181 {% endwith %}
182 182 ]
183 183 [<a href="rss/">RSS</a>]
184 184 </span>
185 185
186 186 {% endblock %}
@@ -1,109 +1,105 b''
1 1 {% load i18n %}
2 2 {% load board %}
3 3
4 4 {% get_current_language as LANGUAGE_CODE %}
5 5
6 6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
7 7 <div class="post-info">
8 8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
9 9 <span class="title">{{ post.title }}</span>
10 10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
11 11 {% if post.tripcode %}
12 12 {% with tripcode=post.get_tripcode %}
13 13 <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}"
14 14 class="tripcode" title="{{ tripcode.get_full_text }}"
15 15 style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a>
16 16 {% endwith %}
17 17 {% endif %}
18 18 {% comment %}
19 19 Thread death time needs to be shown only if the thread is alredy archived
20 20 and this is an opening post (thread death time) or a post for popup
21 21 (we don't see OP here so we show the death time in the post itself).
22 22 {% endcomment %}
23 23 {% if thread.archived %}
24 24 {% if is_opening %}
25 25 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
26 26 {% endif %}
27 27 {% endif %}
28 28 {% if is_opening %}
29 29 {% if need_open_link %}
30 30 {% if thread.archived %}
31 31 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
32 32 {% else %}
33 33 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
34 34 {% endif %}
35 35 {% endif %}
36 36 {% else %}
37 37 {% if need_op_data %}
38 38 {% with thread.get_opening_post as op %}
39 39 {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
40 40 {% endwith %}
41 41 {% endif %}
42 42 {% endif %}
43 43 {% if reply_link and not thread.archived %}
44 44 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
45 45 {% endif %}
46 46
47 47 {% if moderator %}
48 48 <span class="moderator_info">
49 49 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
50 50 {% if is_opening %}
51 51 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
52 52 {% endif %}
53 53 </span>
54 54 {% endif %}
55 55 </div>
56 56 {% comment %}
57 57 Post images. Currently only 1 image can be posted and shown, but post model
58 58 supports multiple.
59 59 {% endcomment %}
60 {% if post.images.exists %}
61 {% with post.images.first as image %}
60 {% for image in post.images.all %}
62 61 {{ image.get_view|safe }}
63 {% endwith %}
64 {% endif %}
65 {% if post.attachments.exists %}
66 {% with post.attachments.first as file %}
62 {% endfor %}
63 {% for file in post.attachments.all %}
67 64 {{ file.get_view|safe }}
68 {% endwith %}
69 {% endif %}
65 {% endfor %}
70 66 {% comment %}
71 67 Post message (text)
72 68 {% endcomment %}
73 69 <div class="message">
74 70 {% autoescape off %}
75 71 {% if truncated %}
76 72 {{ post.get_text|truncatewords_html:50 }}
77 73 {% else %}
78 74 {{ post.get_text }}
79 75 {% endif %}
80 76 {% endautoescape %}
81 77 </div>
82 78 {% if post.is_referenced %}
83 79 {% if mode_tree %}
84 80 <div class="tree_reply">
85 81 {% for refpost in post.get_referenced_posts %}
86 82 {% post_view refpost mode_tree=True %}
87 83 {% endfor %}
88 84 </div>
89 85 {% else %}
90 86 <div class="refmap">
91 87 {% trans "Replies" %}: {{ post.refmap|safe }}
92 88 </div>
93 89 {% endif %}
94 90 {% endif %}
95 91 {% comment %}
96 92 Thread metadata: counters, tags etc
97 93 {% endcomment %}
98 94 {% if is_opening %}
99 95 <div class="metadata">
100 96 {% if is_opening and need_open_link %}
101 97 {% blocktrans count count=thread.get_reply_count %}{{ count }} message{% plural %}{{ count }} messages{% endblocktrans %},
102 98 {% blocktrans count count=thread.get_images_count %}{{ count }} image{% plural %}{{ count }} images{% endblocktrans %}.
103 99 {% endif %}
104 100 <span class="tags">
105 101 {{ thread.get_tag_url_list|safe }}
106 102 </span>
107 103 </div>
108 104 {% endif %}
109 105 </div>
General Comments 0
You need to be logged in to leave comments. Login now