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