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