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