##// END OF EJS Templates
Show thread link and title in the search results and notifications
neko259 -
r1172:353ef0ac default
parent child Browse files
Show More
@@ -1,411 +1,411 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 logging
3 import logging
4 import re
4 import re
5 import uuid
5 import uuid
6
6
7 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.db.models import TextField
10 from django.db.models import TextField
11 from django.template.loader import render_to_string
11 from django.template.loader import render_to_string
12 from django.utils import timezone
12 from django.utils import timezone
13
13
14 from boards import settings
14 from boards import settings
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage
16 from boards.models import PostImage
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards import utils
18 from boards import utils
19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
20 from boards.models.user import Notification, Ban
20 from boards.models.user import Notification, Ban
21 import boards.models.thread
21 import boards.models.thread
22
22
23
23
24 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
25
25
26 POSTS_PER_DAY_RANGE = 7
26 POSTS_PER_DAY_RANGE = 7
27
27
28 BAN_REASON_AUTO = 'Auto'
28 BAN_REASON_AUTO = 'Auto'
29
29
30 IMAGE_THUMB_SIZE = (200, 150)
30 IMAGE_THUMB_SIZE = (200, 150)
31
31
32 TITLE_MAX_LENGTH = 200
32 TITLE_MAX_LENGTH = 200
33
33
34 # TODO This should be removed
34 # TODO This should be removed
35 NO_IP = '0.0.0.0'
35 NO_IP = '0.0.0.0'
36
36
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39
39
40 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TRUNCATED = 'truncated'
41 PARAMETER_TAG = 'tag'
41 PARAMETER_TAG = 'tag'
42 PARAMETER_OFFSET = 'offset'
42 PARAMETER_OFFSET = 'offset'
43 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_DIFF_TYPE = 'type'
44 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_CSS_CLASS = 'css_class'
45 PARAMETER_THREAD = 'thread'
45 PARAMETER_THREAD = 'thread'
46 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_IS_OPENING = 'is_opening'
47 PARAMETER_MODERATOR = 'moderator'
47 PARAMETER_MODERATOR = 'moderator'
48 PARAMETER_POST = 'post'
48 PARAMETER_POST = 'post'
49 PARAMETER_OP_ID = 'opening_post_id'
49 PARAMETER_OP_ID = 'opening_post_id'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53
53
54 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
54 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
55
55
56
56
57 class PostManager(models.Manager):
57 class PostManager(models.Manager):
58 @transaction.atomic
58 @transaction.atomic
59 def create_post(self, title: str, text: str, image=None, thread=None,
59 def create_post(self, title: str, text: str, image=None, thread=None,
60 ip=NO_IP, tags: list=None, threads: list=None):
60 ip=NO_IP, tags: list=None, threads: list=None):
61 """
61 """
62 Creates new post
62 Creates new post
63 """
63 """
64
64
65 is_banned = Ban.objects.filter(ip=ip).exists()
65 is_banned = Ban.objects.filter(ip=ip).exists()
66
66
67 # TODO Raise specific exception and catch it in the views
67 # TODO Raise specific exception and catch it in the views
68 if is_banned:
68 if is_banned:
69 raise Exception("This user is banned")
69 raise Exception("This user is banned")
70
70
71 if not tags:
71 if not tags:
72 tags = []
72 tags = []
73 if not threads:
73 if not threads:
74 threads = []
74 threads = []
75
75
76 posting_time = timezone.now()
76 posting_time = timezone.now()
77 if not thread:
77 if not thread:
78 thread = boards.models.thread.Thread.objects.create(
78 thread = boards.models.thread.Thread.objects.create(
79 bump_time=posting_time, last_edit_time=posting_time)
79 bump_time=posting_time, last_edit_time=posting_time)
80 new_thread = True
80 new_thread = True
81 else:
81 else:
82 new_thread = False
82 new_thread = False
83
83
84 pre_text = Parser().preparse(text)
84 pre_text = Parser().preparse(text)
85
85
86 post = self.create(title=title,
86 post = self.create(title=title,
87 text=pre_text,
87 text=pre_text,
88 pub_time=posting_time,
88 pub_time=posting_time,
89 poster_ip=ip,
89 poster_ip=ip,
90 thread=thread,
90 thread=thread,
91 last_edit_time=posting_time)
91 last_edit_time=posting_time)
92 post.threads.add(thread)
92 post.threads.add(thread)
93
93
94 logger = logging.getLogger('boards.post.create')
94 logger = logging.getLogger('boards.post.create')
95
95
96 logger.info('Created post {} by {}'.format(post, post.poster_ip))
96 logger.info('Created post {} by {}'.format(post, post.poster_ip))
97
97
98 if image:
98 if image:
99 post.images.add(PostImage.objects.create_with_hash(image))
99 post.images.add(PostImage.objects.create_with_hash(image))
100
100
101 list(map(thread.add_tag, tags))
101 list(map(thread.add_tag, tags))
102
102
103 if new_thread:
103 if new_thread:
104 boards.models.thread.Thread.objects.process_oldest_threads()
104 boards.models.thread.Thread.objects.process_oldest_threads()
105 else:
105 else:
106 thread.last_edit_time = posting_time
106 thread.last_edit_time = posting_time
107 thread.bump()
107 thread.bump()
108 thread.save()
108 thread.save()
109
109
110 post.connect_replies()
110 post.connect_replies()
111 post.connect_threads(threads)
111 post.connect_threads(threads)
112 post.connect_notifications()
112 post.connect_notifications()
113
113
114 post.build_url()
114 post.build_url()
115
115
116 return post
116 return post
117
117
118 def delete_posts_by_ip(self, ip):
118 def delete_posts_by_ip(self, ip):
119 """
119 """
120 Deletes all posts of the author with same IP
120 Deletes all posts of the author with same IP
121 """
121 """
122
122
123 posts = self.filter(poster_ip=ip)
123 posts = self.filter(poster_ip=ip)
124 for post in posts:
124 for post in posts:
125 post.delete()
125 post.delete()
126
126
127 @utils.cached_result()
127 @utils.cached_result()
128 def get_posts_per_day(self) -> float:
128 def get_posts_per_day(self) -> float:
129 """
129 """
130 Gets average count of posts per day for the last 7 days
130 Gets average count of posts per day for the last 7 days
131 """
131 """
132
132
133 day_end = date.today()
133 day_end = date.today()
134 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
134 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
135
135
136 day_time_start = timezone.make_aware(datetime.combine(
136 day_time_start = timezone.make_aware(datetime.combine(
137 day_start, dtime()), timezone.get_current_timezone())
137 day_start, dtime()), timezone.get_current_timezone())
138 day_time_end = timezone.make_aware(datetime.combine(
138 day_time_end = timezone.make_aware(datetime.combine(
139 day_end, dtime()), timezone.get_current_timezone())
139 day_end, dtime()), timezone.get_current_timezone())
140
140
141 posts_per_period = float(self.filter(
141 posts_per_period = float(self.filter(
142 pub_time__lte=day_time_end,
142 pub_time__lte=day_time_end,
143 pub_time__gte=day_time_start).count())
143 pub_time__gte=day_time_start).count())
144
144
145 ppd = posts_per_period / POSTS_PER_DAY_RANGE
145 ppd = posts_per_period / POSTS_PER_DAY_RANGE
146
146
147 return ppd
147 return ppd
148
148
149
149
150 class Post(models.Model, Viewable):
150 class Post(models.Model, Viewable):
151 """A post is a message."""
151 """A post is a message."""
152
152
153 objects = PostManager()
153 objects = PostManager()
154
154
155 class Meta:
155 class Meta:
156 app_label = APP_LABEL_BOARDS
156 app_label = APP_LABEL_BOARDS
157 ordering = ('id',)
157 ordering = ('id',)
158
158
159 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
159 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
160 pub_time = models.DateTimeField()
160 pub_time = models.DateTimeField()
161 text = TextField(blank=True, null=True)
161 text = TextField(blank=True, null=True)
162 _text_rendered = TextField(blank=True, null=True, editable=False)
162 _text_rendered = TextField(blank=True, null=True, editable=False)
163
163
164 images = models.ManyToManyField(PostImage, null=True, blank=True,
164 images = models.ManyToManyField(PostImage, null=True, blank=True,
165 related_name='ip+', db_index=True)
165 related_name='ip+', db_index=True)
166
166
167 poster_ip = models.GenericIPAddressField()
167 poster_ip = models.GenericIPAddressField()
168
168
169 # TODO This field can be removed cause UID is used for update now
169 # TODO This field can be removed cause UID is used for update now
170 last_edit_time = models.DateTimeField()
170 last_edit_time = models.DateTimeField()
171
171
172 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
172 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
173 null=True,
173 null=True,
174 blank=True, related_name='rfp+',
174 blank=True, related_name='rfp+',
175 db_index=True)
175 db_index=True)
176 refmap = models.TextField(null=True, blank=True)
176 refmap = models.TextField(null=True, blank=True)
177 threads = models.ManyToManyField('Thread', db_index=True)
177 threads = models.ManyToManyField('Thread', db_index=True)
178 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
178 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
179
179
180 url = models.TextField()
180 url = models.TextField()
181 uid = models.TextField(db_index=True)
181 uid = models.TextField(db_index=True)
182
182
183 def __str__(self):
183 def __str__(self):
184 return 'P#{}/{}'.format(self.id, self.title)
184 return 'P#{}/{}'.format(self.id, self.title)
185
185
186 def get_title(self) -> str:
186 def get_title(self) -> str:
187 """
187 """
188 Gets original post title or part of its text.
188 Gets original post title or part of its text.
189 """
189 """
190
190
191 title = self.title
191 title = self.title
192 if not title:
192 if not title:
193 title = self.get_text()
193 title = self.get_text()
194
194
195 return title
195 return title
196
196
197 def build_refmap(self) -> None:
197 def build_refmap(self) -> None:
198 """
198 """
199 Builds a replies map string from replies list. This is a cache to stop
199 Builds a replies map string from replies list. This is a cache to stop
200 the server from recalculating the map on every post show.
200 the server from recalculating the map on every post show.
201 """
201 """
202
202
203 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
203 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
204 for refpost in self.referenced_posts.all()]
204 for refpost in self.referenced_posts.all()]
205
205
206 self.refmap = ', '.join(post_urls)
206 self.refmap = ', '.join(post_urls)
207
207
208 def is_referenced(self) -> bool:
208 def is_referenced(self) -> bool:
209 return self.refmap and len(self.refmap) > 0
209 return self.refmap and len(self.refmap) > 0
210
210
211 def is_opening(self) -> bool:
211 def is_opening(self) -> bool:
212 """
212 """
213 Checks if this is an opening post or just a reply.
213 Checks if this is an opening post or just a reply.
214 """
214 """
215
215
216 return self.get_thread().get_opening_post_id() == self.id
216 return self.get_thread().get_opening_post_id() == self.id
217
217
218 def get_absolute_url(self):
218 def get_absolute_url(self):
219 return self.url
219 return self.url
220
220
221 def get_thread(self):
221 def get_thread(self):
222 return self.thread
222 return self.thread
223
223
224 def get_threads(self) -> list:
224 def get_threads(self) -> list:
225 """
225 """
226 Gets post's thread.
226 Gets post's thread.
227 """
227 """
228
228
229 return self.threads
229 return self.threads
230
230
231 def get_view(self, moderator=False, need_open_link=False,
231 def get_view(self, moderator=False, need_open_link=False,
232 truncated=False, reply_link=False, *args, **kwargs) -> str:
232 truncated=False, reply_link=False, *args, **kwargs) -> str:
233 """
233 """
234 Renders post's HTML view. Some of the post params can be passed over
234 Renders post's HTML view. Some of the post params can be passed over
235 kwargs for the means of caching (if we view the thread, some params
235 kwargs for the means of caching (if we view the thread, some params
236 are same for every post and don't need to be computed over and over.
236 are same for every post and don't need to be computed over and over.
237 """
237 """
238
238
239 thread = self.get_thread()
239 thread = self.get_thread()
240 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
240 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
241
241
242 if is_opening:
242 if is_opening:
243 opening_post_id = self.id
243 opening_post_id = self.id
244 else:
244 else:
245 opening_post_id = thread.get_opening_post_id()
245 opening_post_id = thread.get_opening_post_id()
246
246
247 css_class = 'post'
247 css_class = 'post'
248 if thread.archived:
248 if thread.archived:
249 css_class += ' archive_post'
249 css_class += ' archive_post'
250 elif not thread.can_bump():
250 elif not thread.can_bump():
251 css_class += ' dead_post'
251 css_class += ' dead_post'
252
252
253 return render_to_string('boards/post.html', {
253 return render_to_string('boards/post.html', {
254 PARAMETER_POST: self,
254 PARAMETER_POST: self,
255 PARAMETER_MODERATOR: moderator,
255 PARAMETER_MODERATOR: moderator,
256 PARAMETER_IS_OPENING: is_opening,
256 PARAMETER_IS_OPENING: is_opening,
257 PARAMETER_THREAD: thread,
257 PARAMETER_THREAD: thread,
258 PARAMETER_CSS_CLASS: css_class,
258 PARAMETER_CSS_CLASS: css_class,
259 PARAMETER_NEED_OPEN_LINK: need_open_link,
259 PARAMETER_NEED_OPEN_LINK: need_open_link,
260 PARAMETER_TRUNCATED: truncated,
260 PARAMETER_TRUNCATED: truncated,
261 PARAMETER_OP_ID: opening_post_id,
261 PARAMETER_OP_ID: opening_post_id,
262 PARAMETER_REPLY_LINK: reply_link,
262 PARAMETER_REPLY_LINK: reply_link,
263 PARAMETER_NEED_OP_DATA: kwargs.get(PARAMETER_NEED_OP_DATA)
263 PARAMETER_NEED_OP_DATA: kwargs.get(PARAMETER_NEED_OP_DATA)
264 })
264 })
265
265
266 def get_search_view(self, *args, **kwargs):
266 def get_search_view(self, *args, **kwargs):
267 return self.get_view(args, kwargs)
267 return self.get_view(need_op_data=True, *args, **kwargs)
268
268
269 def get_first_image(self) -> PostImage:
269 def get_first_image(self) -> PostImage:
270 return self.images.earliest('id')
270 return self.images.earliest('id')
271
271
272 def delete(self, using=None):
272 def delete(self, using=None):
273 """
273 """
274 Deletes all post images and the post itself.
274 Deletes all post images and the post itself.
275 """
275 """
276
276
277 for image in self.images.all():
277 for image in self.images.all():
278 image_refs_count = Post.objects.filter(images__in=[image]).count()
278 image_refs_count = Post.objects.filter(images__in=[image]).count()
279 if image_refs_count == 1:
279 if image_refs_count == 1:
280 image.delete()
280 image.delete()
281
281
282 thread = self.get_thread()
282 thread = self.get_thread()
283 thread.last_edit_time = timezone.now()
283 thread.last_edit_time = timezone.now()
284 thread.save()
284 thread.save()
285
285
286 super(Post, self).delete(using)
286 super(Post, self).delete(using)
287
287
288 logging.getLogger('boards.post.delete').info(
288 logging.getLogger('boards.post.delete').info(
289 'Deleted post {}'.format(self))
289 'Deleted post {}'.format(self))
290
290
291 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
291 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
292 include_last_update=False) -> str:
292 include_last_update=False) -> str:
293 """
293 """
294 Gets post HTML or JSON data that can be rendered on a page or used by
294 Gets post HTML or JSON data that can be rendered on a page or used by
295 API.
295 API.
296 """
296 """
297
297
298 return get_exporter(format_type).export(self, request,
298 return get_exporter(format_type).export(self, request,
299 include_last_update)
299 include_last_update)
300
300
301 def notify_clients(self, recursive=True):
301 def notify_clients(self, recursive=True):
302 """
302 """
303 Sends post HTML data to the thread web socket.
303 Sends post HTML data to the thread web socket.
304 """
304 """
305
305
306 if not settings.get_bool('External', 'WebsocketsEnabled'):
306 if not settings.get_bool('External', 'WebsocketsEnabled'):
307 return
307 return
308
308
309 thread_ids = list()
309 thread_ids = list()
310 for thread in self.get_threads().all():
310 for thread in self.get_threads().all():
311 thread_ids.append(thread.id)
311 thread_ids.append(thread.id)
312
312
313 thread.notify_clients()
313 thread.notify_clients()
314
314
315 if recursive:
315 if recursive:
316 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
316 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
317 post_id = reply_number.group(1)
317 post_id = reply_number.group(1)
318
318
319 try:
319 try:
320 ref_post = Post.objects.get(id=post_id)
320 ref_post = Post.objects.get(id=post_id)
321
321
322 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
322 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
323 # If post is in this thread, its thread was already notified.
323 # If post is in this thread, its thread was already notified.
324 # Otherwise, notify its thread separately.
324 # Otherwise, notify its thread separately.
325 ref_post.notify_clients(recursive=False)
325 ref_post.notify_clients(recursive=False)
326 except ObjectDoesNotExist:
326 except ObjectDoesNotExist:
327 pass
327 pass
328
328
329 def build_url(self):
329 def build_url(self):
330 thread = self.get_thread()
330 thread = self.get_thread()
331 opening_id = thread.get_opening_post_id()
331 opening_id = thread.get_opening_post_id()
332 post_url = reverse('thread', kwargs={'post_id': opening_id})
332 post_url = reverse('thread', kwargs={'post_id': opening_id})
333 if self.id != opening_id:
333 if self.id != opening_id:
334 post_url += '#' + str(self.id)
334 post_url += '#' + str(self.id)
335 self.url = post_url
335 self.url = post_url
336 self.save(update_fields=['url'])
336 self.save(update_fields=['url'])
337
337
338 def save(self, force_insert=False, force_update=False, using=None,
338 def save(self, force_insert=False, force_update=False, using=None,
339 update_fields=None):
339 update_fields=None):
340 self._text_rendered = Parser().parse(self.get_raw_text())
340 self._text_rendered = Parser().parse(self.get_raw_text())
341
341
342 self.uid = str(uuid.uuid4())
342 self.uid = str(uuid.uuid4())
343 if update_fields is not None and 'uid' not in update_fields:
343 if update_fields is not None and 'uid' not in update_fields:
344 update_fields += ['uid']
344 update_fields += ['uid']
345
345
346 if self.id:
346 if self.id:
347 for thread in self.get_threads().all():
347 for thread in self.get_threads().all():
348 if thread.can_bump():
348 if thread.can_bump():
349 thread.update_bump_status(exclude_posts=[self])
349 thread.update_bump_status(exclude_posts=[self])
350 thread.last_edit_time = self.last_edit_time
350 thread.last_edit_time = self.last_edit_time
351
351
352 thread.save(update_fields=['last_edit_time', 'bumpable'])
352 thread.save(update_fields=['last_edit_time', 'bumpable'])
353
353
354 super().save(force_insert, force_update, using, update_fields)
354 super().save(force_insert, force_update, using, update_fields)
355
355
356 def get_text(self) -> str:
356 def get_text(self) -> str:
357 return self._text_rendered
357 return self._text_rendered
358
358
359 def get_raw_text(self) -> str:
359 def get_raw_text(self) -> str:
360 return self.text
360 return self.text
361
361
362 def get_absolute_id(self) -> str:
362 def get_absolute_id(self) -> str:
363 """
363 """
364 If the post has many threads, shows its main thread OP id in the post
364 If the post has many threads, shows its main thread OP id in the post
365 ID.
365 ID.
366 """
366 """
367
367
368 if self.get_threads().count() > 1:
368 if self.get_threads().count() > 1:
369 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
369 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
370 else:
370 else:
371 return str(self.id)
371 return str(self.id)
372
372
373 def connect_notifications(self):
373 def connect_notifications(self):
374 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
374 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
375 user_name = reply_number.group(1).lower()
375 user_name = reply_number.group(1).lower()
376 Notification.objects.get_or_create(name=user_name, post=self)
376 Notification.objects.get_or_create(name=user_name, post=self)
377
377
378 def connect_replies(self):
378 def connect_replies(self):
379 """
379 """
380 Connects replies to a post to show them as a reflink map
380 Connects replies to a post to show them as a reflink map
381 """
381 """
382
382
383 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
383 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
384 post_id = reply_number.group(1)
384 post_id = reply_number.group(1)
385
385
386 try:
386 try:
387 referenced_post = Post.objects.get(id=post_id)
387 referenced_post = Post.objects.get(id=post_id)
388
388
389 referenced_post.referenced_posts.add(self)
389 referenced_post.referenced_posts.add(self)
390 referenced_post.last_edit_time = self.pub_time
390 referenced_post.last_edit_time = self.pub_time
391 referenced_post.build_refmap()
391 referenced_post.build_refmap()
392 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
392 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
393 except ObjectDoesNotExist:
393 except ObjectDoesNotExist:
394 pass
394 pass
395
395
396 def connect_threads(self, opening_posts):
396 def connect_threads(self, opening_posts):
397 """
397 """
398 If the referenced post is an OP in another thread,
398 If the referenced post is an OP in another thread,
399 make this post multi-thread.
399 make this post multi-thread.
400 """
400 """
401
401
402 for opening_post in opening_posts:
402 for opening_post in opening_posts:
403 threads = opening_post.get_threads().all()
403 threads = opening_post.get_threads().all()
404 for thread in threads:
404 for thread in threads:
405 if thread.can_bump():
405 if thread.can_bump():
406 thread.update_bump_status()
406 thread.update_bump_status()
407
407
408 thread.last_edit_time = self.last_edit_time
408 thread.last_edit_time = self.last_edit_time
409 thread.save(update_fields=['last_edit_time', 'bumpable'])
409 thread.save(update_fields=['last_edit_time', 'bumpable'])
410
410
411 self.threads.add(thread)
411 self.threads.add(thread)
@@ -1,31 +1,31 b''
1 {% extends 'boards/base.html' %}
1 {% extends 'boards/base.html' %}
2
2
3 {% load board %}
3 {% load board %}
4 {% load i18n %}
4 {% load i18n %}
5
5
6 {% block head %}
6 {% block head %}
7 <meta name="robots" content="noindex">
7 <meta name="robots" content="noindex">
8 <title>{{ site_name }} - {% trans 'Notifications' %} - {{ notification_username }}</title>
8 <title>{{ site_name }} - {% trans 'Notifications' %} - {{ notification_username }}</title>
9 {% endblock %}
9 {% endblock %}
10
10
11 {% block content %}
11 {% block content %}
12 <div class="tag_info"><a href="{% url 'notifications' notification_username %}" class="user-cast">@{{ notification_username }}</a></div>
12 <div class="tag_info"><a href="{% url 'notifications' notification_username %}" class="user-cast">@{{ notification_username }}</a></div>
13
13
14 {% if page %}
14 {% if page %}
15 {% if page.has_previous %}
15 {% if page.has_previous %}
16 <div class="page_link">
16 <div class="page_link">
17 <a href="?page={{ page.previous_page_number }}">{% trans "Previous page" %}</a>
17 <a href="?page={{ page.previous_page_number }}">{% trans "Previous page" %}</a>
18 </div>
18 </div>
19 {% endif %}
19 {% endif %}
20
20
21 {% for post in page.object_list %}
21 {% for post in page.object_list %}
22 {% post_view post %}
22 {% post_view post need_op_data=True %}
23 {% endfor %}
23 {% endfor %}
24
24
25 {% if page.has_next %}
25 {% if page.has_next %}
26 <div class="page_link">
26 <div class="page_link">
27 <a href="?page={{ page.next_page_number }}">{% trans "Next page" %}</a>
27 <a href="?page={{ page.next_page_number }}">{% trans "Next page" %}</a>
28 </div>
28 </div>
29 {% endif %}
29 {% endif %}
30 {% endif %}
30 {% endif %}
31 {% endblock %}
31 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now