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