##// END OF EJS Templates
Define signals outside of the post model
neko259 -
r1512:2edb321f decentral
parent child Browse files
Show More
@@ -0,0 +1,11 b''
1 from django.apps import AppConfig
2
3
4 class BoardsAppConfig(AppConfig):
5 name = 'boards'
6 verbose_name = 'Boards'
7
8 def ready(self):
9 super().ready()
10
11 import boards.signals No newline at end of file
@@ -0,0 +1,62 b''
1 import re
2 from boards.mdx_neboard import get_parser
3
4 from boards.models import Post
5 from boards.models.post import REGEX_NOTIFICATION
6 from boards.models.post import REGEX_REPLY
7 from boards.models.user import Notification
8 from django.db.models.signals import post_save, pre_save, pre_delete, \
9 post_delete
10 from django.dispatch import receiver
11 from django.utils import timezone
12
13
14 @receiver(post_save, sender=Post)
15 def connect_replies(instance, **kwargs):
16 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
17 post_id = reply_number.group(1)
18
19 try:
20 referenced_post = Post.objects.get(id=post_id)
21
22 referenced_post.referenced_posts.add(instance)
23 referenced_post.last_edit_time = instance.pub_time
24 referenced_post.build_refmap()
25 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
26 except Post.ObjectDoesNotExist:
27 pass
28
29
30 @receiver(post_save, sender=Post)
31 def connect_notifications(instance, **kwargs):
32 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
33 user_name = reply_number.group(1).lower()
34 Notification.objects.get_or_create(name=user_name, post=instance)
35
36
37 @receiver(pre_save, sender=Post)
38 def preparse_text(instance, **kwargs):
39 instance._text_rendered = get_parser().parse(instance.get_raw_text())
40
41
42 @receiver(pre_delete, sender=Post)
43 def delete_images(instance, **kwargs):
44 for image in instance.images.all():
45 image_refs_count = image.post_images.count()
46 if image_refs_count == 1:
47 image.delete()
48
49
50 @receiver(pre_delete, sender=Post)
51 def delete_attachments(instance, **kwargs):
52 for attachment in instance.attachments.all():
53 attachment_refs_count = attachment.attachment_posts.count()
54 if attachment_refs_count == 1:
55 attachment.delete()
56
57
58 @receiver(post_delete, sender=Post)
59 def update_thread_on_delete(instance, **kwargs):
60 thread = instance.get_thread()
61 thread.last_edit_time = timezone.now()
62 thread.save()
@@ -0,0 +1,1 b''
1 default_app_config = 'boards.apps.BoardsAppConfig' No newline at end of file
@@ -1,453 +1,401 b''
1 1 import logging
2 2 import uuid
3 3
4 4 import re
5 5 from boards import settings
6 6 from boards.abstracts.tripcode import Tripcode
7 7 from boards.mdx_neboard import get_parser
8 8 from boards.models import PostImage, Attachment, KeyPair, GlobalId
9 9 from boards.models.base import Viewable
10 10 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
11 11 from boards.models.post.manager import PostManager
12 12 from boards.models.user import Notification
13 13 from django.core.exceptions import ObjectDoesNotExist
14 14 from django.core.urlresolvers import reverse
15 15 from django.db import models
16 16 from django.db.models import TextField, QuerySet
17 17 from django.db.models.signals import pre_save, post_save, pre_delete, \
18 18 post_delete
19 19 from django.dispatch import receiver
20 20 from django.template.defaultfilters import truncatewords, striptags
21 21 from django.template.loader import render_to_string
22 22 from django.utils import timezone
23 23
24 24 CSS_CLS_HIDDEN_POST = 'hidden_post'
25 25 CSS_CLS_DEAD_POST = 'dead_post'
26 26 CSS_CLS_ARCHIVE_POST = 'archive_post'
27 27 CSS_CLS_POST = 'post'
28 28 CSS_CLS_MONOCHROME = 'monochrome'
29 29
30 30 TITLE_MAX_WORDS = 10
31 31
32 32 APP_LABEL_BOARDS = 'boards'
33 33
34 34 BAN_REASON_AUTO = 'Auto'
35 35
36 36 IMAGE_THUMB_SIZE = (200, 150)
37 37
38 38 TITLE_MAX_LENGTH = 200
39 39
40 40 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
41 41 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
42 42 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
43 43 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
44 44
45 45 PARAMETER_TRUNCATED = 'truncated'
46 46 PARAMETER_TAG = 'tag'
47 47 PARAMETER_OFFSET = 'offset'
48 48 PARAMETER_DIFF_TYPE = 'type'
49 49 PARAMETER_CSS_CLASS = 'css_class'
50 50 PARAMETER_THREAD = 'thread'
51 51 PARAMETER_IS_OPENING = 'is_opening'
52 52 PARAMETER_POST = 'post'
53 53 PARAMETER_OP_ID = 'opening_post_id'
54 54 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
55 55 PARAMETER_REPLY_LINK = 'reply_link'
56 56 PARAMETER_NEED_OP_DATA = 'need_op_data'
57 57
58 58 POST_VIEW_PARAMS = (
59 59 'need_op_data',
60 60 'reply_link',
61 61 'need_open_link',
62 62 'truncated',
63 63 'mode_tree',
64 64 'perms',
65 65 'tree_depth',
66 66 )
67 67
68 68
69 69 class Post(models.Model, Viewable):
70 70 """A post is a message."""
71 71
72 72 objects = PostManager()
73 73
74 74 class Meta:
75 75 app_label = APP_LABEL_BOARDS
76 76 ordering = ('id',)
77 77
78 78 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
79 79 pub_time = models.DateTimeField()
80 80 text = TextField(blank=True, null=True)
81 81 _text_rendered = TextField(blank=True, null=True, editable=False)
82 82
83 83 images = models.ManyToManyField(PostImage, null=True, blank=True,
84 84 related_name='post_images', db_index=True)
85 85 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
86 86 related_name='attachment_posts')
87 87
88 88 poster_ip = models.GenericIPAddressField()
89 89
90 90 # TODO This field can be removed cause UID is used for update now
91 91 last_edit_time = models.DateTimeField()
92 92
93 93 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
94 94 null=True,
95 95 blank=True, related_name='refposts',
96 96 db_index=True)
97 97 refmap = models.TextField(null=True, blank=True)
98 98 threads = models.ManyToManyField('Thread', db_index=True,
99 99 related_name='multi_replies')
100 100 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
101 101
102 102 url = models.TextField()
103 103 uid = models.TextField(db_index=True)
104 104
105 105 # Global ID with author key. If the message was downloaded from another
106 106 # server, this indicates the server.
107 107 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
108 108 on_delete=models.CASCADE)
109 109
110 110 tripcode = models.CharField(max_length=50, blank=True, default='')
111 111 opening = models.BooleanField(db_index=True)
112 112 hidden = models.BooleanField(default=False)
113 113
114 114 def __str__(self):
115 115 return 'P#{}/{}'.format(self.id, self.get_title())
116 116
117 117 def get_referenced_posts(self):
118 118 threads = self.get_threads().all()
119 119 return self.referenced_posts.filter(threads__in=threads)\
120 120 .order_by('pub_time').distinct().all()
121 121
122 122 def get_title(self) -> str:
123 123 return self.title
124 124
125 125 def get_title_or_text(self):
126 126 title = self.get_title()
127 127 if not title:
128 128 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
129 129
130 130 return title
131 131
132 132 def build_refmap(self) -> None:
133 133 """
134 134 Builds a replies map string from replies list. This is a cache to stop
135 135 the server from recalculating the map on every post show.
136 136 """
137 137
138 138 post_urls = [refpost.get_link_view()
139 139 for refpost in self.referenced_posts.all()]
140 140
141 141 self.refmap = ', '.join(post_urls)
142 142
143 143 def is_referenced(self) -> bool:
144 144 return self.refmap and len(self.refmap) > 0
145 145
146 146 def is_opening(self) -> bool:
147 147 """
148 148 Checks if this is an opening post or just a reply.
149 149 """
150 150
151 151 return self.opening
152 152
153 153 def get_absolute_url(self, thread=None):
154 154 url = None
155 155
156 156 if thread is None:
157 157 thread = self.get_thread()
158 158
159 159 # Url is cached only for the "main" thread. When getting url
160 160 # for other threads, do it manually.
161 161 if self.url:
162 162 url = self.url
163 163
164 164 if url is None:
165 165 opening = self.is_opening()
166 166 opening_id = self.id if opening else thread.get_opening_post_id()
167 167 url = reverse('thread', kwargs={'post_id': opening_id})
168 168 if not opening:
169 169 url += '#' + str(self.id)
170 170
171 171 return url
172 172
173 173 def get_thread(self):
174 174 return self.thread
175 175
176 176 def get_thread_id(self):
177 177 return self.thread_id
178 178
179 179 def get_threads(self) -> QuerySet:
180 180 """
181 181 Gets post's thread.
182 182 """
183 183
184 184 return self.threads
185 185
186 186 def get_view(self, *args, **kwargs) -> str:
187 187 """
188 188 Renders post's HTML view. Some of the post params can be passed over
189 189 kwargs for the means of caching (if we view the thread, some params
190 190 are same for every post and don't need to be computed over and over.
191 191 """
192 192
193 193 thread = self.get_thread()
194 194
195 195 css_classes = [CSS_CLS_POST]
196 196 if thread.is_archived():
197 197 css_classes.append(CSS_CLS_ARCHIVE_POST)
198 198 elif not thread.can_bump():
199 199 css_classes.append(CSS_CLS_DEAD_POST)
200 200 if self.is_hidden():
201 201 css_classes.append(CSS_CLS_HIDDEN_POST)
202 202 if thread.is_monochrome():
203 203 css_classes.append(CSS_CLS_MONOCHROME)
204 204
205 205 params = dict()
206 206 for param in POST_VIEW_PARAMS:
207 207 if param in kwargs:
208 208 params[param] = kwargs[param]
209 209
210 210 params.update({
211 211 PARAMETER_POST: self,
212 212 PARAMETER_IS_OPENING: self.is_opening(),
213 213 PARAMETER_THREAD: thread,
214 214 PARAMETER_CSS_CLASS: ' '.join(css_classes),
215 215 })
216 216
217 217 return render_to_string('boards/post.html', params)
218 218
219 219 def get_search_view(self, *args, **kwargs):
220 220 return self.get_view(need_op_data=True, *args, **kwargs)
221 221
222 222 def get_first_image(self) -> PostImage:
223 223 return self.images.earliest('id')
224 224
225 225 def set_global_id(self, key_pair=None):
226 226 """
227 227 Sets global id based on the given key pair. If no key pair is given,
228 228 default one is used.
229 229 """
230 230
231 231 if key_pair:
232 232 key = key_pair
233 233 else:
234 234 try:
235 235 key = KeyPair.objects.get(primary=True)
236 236 except KeyPair.DoesNotExist:
237 237 # Do not update the global id because there is no key defined
238 238 return
239 239 global_id = GlobalId(key_type=key.key_type,
240 240 key=key.public_key,
241 241 local_id=self.id)
242 242 global_id.save()
243 243
244 244 self.global_id = global_id
245 245
246 246 self.save(update_fields=['global_id'])
247 247
248 248 def get_pub_time_str(self):
249 249 return str(self.pub_time)
250 250
251 251 def get_replied_ids(self):
252 252 """
253 253 Gets ID list of the posts that this post replies.
254 254 """
255 255
256 256 raw_text = self.get_raw_text()
257 257
258 258 local_replied = REGEX_REPLY.findall(raw_text)
259 259 global_replied = []
260 260 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
261 261 key_type = match[0]
262 262 key = match[1]
263 263 local_id = match[2]
264 264
265 265 try:
266 266 global_id = GlobalId.objects.get(key_type=key_type,
267 267 key=key, local_id=local_id)
268 268 for post in Post.objects.filter(global_id=global_id).only('id'):
269 269 global_replied.append(post.id)
270 270 except GlobalId.DoesNotExist:
271 271 pass
272 272 return local_replied + global_replied
273 273
274 274 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
275 275 include_last_update=False) -> str:
276 276 """
277 277 Gets post HTML or JSON data that can be rendered on a page or used by
278 278 API.
279 279 """
280 280
281 281 return get_exporter(format_type).export(self, request,
282 282 include_last_update)
283 283
284 284 def notify_clients(self, recursive=True):
285 285 """
286 286 Sends post HTML data to the thread web socket.
287 287 """
288 288
289 289 if not settings.get_bool('External', 'WebsocketsEnabled'):
290 290 return
291 291
292 292 thread_ids = list()
293 293 for thread in self.get_threads().all():
294 294 thread_ids.append(thread.id)
295 295
296 296 thread.notify_clients()
297 297
298 298 if recursive:
299 299 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
300 300 post_id = reply_number.group(1)
301 301
302 302 try:
303 303 ref_post = Post.objects.get(id=post_id)
304 304
305 305 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
306 306 # If post is in this thread, its thread was already notified.
307 307 # Otherwise, notify its thread separately.
308 308 ref_post.notify_clients(recursive=False)
309 309 except ObjectDoesNotExist:
310 310 pass
311 311
312 312 def build_url(self):
313 313 self.url = self.get_absolute_url()
314 314 self.save(update_fields=['url'])
315 315
316 316 def save(self, force_insert=False, force_update=False, using=None,
317 317 update_fields=None):
318 318 new_post = self.id is None
319 319
320 320 self.uid = str(uuid.uuid4())
321 321 if update_fields is not None and 'uid' not in update_fields:
322 322 update_fields += ['uid']
323 323
324 324 if not new_post:
325 325 for thread in self.get_threads().all():
326 326 thread.last_edit_time = self.last_edit_time
327 327
328 328 thread.save(update_fields=['last_edit_time', 'status'])
329 329
330 330 super().save(force_insert, force_update, using, update_fields)
331 331
332 332 if self.url is None:
333 333 self.build_url()
334 334
335 335 def get_text(self) -> str:
336 336 return self._text_rendered
337 337
338 338 def get_raw_text(self) -> str:
339 339 return self.text
340 340
341 341 def get_sync_text(self) -> str:
342 342 """
343 343 Returns text applicable for sync. It has absolute post reflinks.
344 344 """
345 345
346 346 replacements = dict()
347 347 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
348 348 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
349 349 replacements[post_id] = absolute_post_id
350 350
351 351 text = self.get_raw_text() or ''
352 352 for key in replacements:
353 353 text = text.replace('[post]{}[/post]'.format(key),
354 354 '[post]{}[/post]'.format(replacements[key]))
355 355 text = text.replace('\r\n', '\n').replace('\r', '\n')
356 356
357 357 return text
358 358
359 359 def get_absolute_id(self) -> str:
360 360 """
361 361 If the post has many threads, shows its main thread OP id in the post
362 362 ID.
363 363 """
364 364
365 365 if self.get_threads().count() > 1:
366 366 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
367 367 else:
368 368 return str(self.id)
369 369
370 370
371 371 def connect_threads(self, opening_posts):
372 372 for opening_post in opening_posts:
373 373 threads = opening_post.get_threads().all()
374 374 for thread in threads:
375 375 if thread.can_bump():
376 376 thread.update_bump_status()
377 377
378 378 thread.last_edit_time = self.last_edit_time
379 379 thread.save(update_fields=['last_edit_time', 'status'])
380 380 self.threads.add(opening_post.get_thread())
381 381
382 382 def get_tripcode(self):
383 383 if self.tripcode:
384 384 return Tripcode(self.tripcode)
385 385
386 386 def get_link_view(self):
387 387 """
388 388 Gets view of a reflink to the post.
389 389 """
390 390 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
391 391 self.id)
392 392 if self.is_opening():
393 393 result = '<b>{}</b>'.format(result)
394 394
395 395 return result
396 396
397 397 def is_hidden(self) -> bool:
398 398 return self.hidden
399 399
400 400 def set_hidden(self, hidden):
401 401 self.hidden = hidden
402
403
404 # SIGNALS (Maybe move to other module?)
405 @receiver(post_save, sender=Post)
406 def connect_replies(instance, **kwargs):
407 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
408 post_id = reply_number.group(1)
409
410 try:
411 referenced_post = Post.objects.get(id=post_id)
412
413 referenced_post.referenced_posts.add(instance)
414 referenced_post.last_edit_time = instance.pub_time
415 referenced_post.build_refmap()
416 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
417 except ObjectDoesNotExist:
418 pass
419
420
421 @receiver(post_save, sender=Post)
422 def connect_notifications(instance, **kwargs):
423 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
424 user_name = reply_number.group(1).lower()
425 Notification.objects.get_or_create(name=user_name, post=instance)
426
427
428 @receiver(pre_save, sender=Post)
429 def preparse_text(instance, **kwargs):
430 instance._text_rendered = get_parser().parse(instance.get_raw_text())
431
432
433 @receiver(pre_delete, sender=Post)
434 def delete_images(instance, **kwargs):
435 for image in instance.images.all():
436 image_refs_count = image.post_images.count()
437 if image_refs_count == 1:
438 image.delete()
439
440
441 @receiver(pre_delete, sender=Post)
442 def delete_attachments(instance, **kwargs):
443 for attachment in instance.attachments.all():
444 attachment_refs_count = attachment.attachment_posts.count()
445 if attachment_refs_count == 1:
446 attachment.delete()
447
448
449 @receiver(post_delete, sender=Post)
450 def update_thread_on_delete(instance, **kwargs):
451 thread = instance.get_thread()
452 thread.last_edit_time = timezone.now()
453 thread.save()
General Comments 0
You need to be logged in to leave comments. Login now