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