##// END OF EJS Templates
Fixed issue with newly added posts appearance
neko259 -
r1650:8a0acd9f default
parent child Browse files
Show More
@@ -1,410 +1,410 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 183 thread = self.get_thread()
184 184
185 185 css_classes = [CSS_CLS_POST]
186 186 if thread.is_archived():
187 187 css_classes.append(CSS_CLS_ARCHIVE_POST)
188 188 elif not thread.can_bump():
189 189 css_classes.append(CSS_CLS_DEAD_POST)
190 190 if self.is_hidden():
191 191 css_classes.append(CSS_CLS_HIDDEN_POST)
192 192 if thread.is_monochrome():
193 193 css_classes.append(CSS_CLS_MONOCHROME)
194 194
195 195 params = dict()
196 196 for param in POST_VIEW_PARAMS:
197 197 if param in kwargs:
198 198 params[param] = kwargs[param]
199 199
200 200 params.update({
201 201 PARAMETER_POST: self,
202 202 PARAMETER_IS_OPENING: self.is_opening(),
203 203 PARAMETER_THREAD: thread,
204 204 PARAMETER_CSS_CLASS: ' '.join(css_classes),
205 205 })
206 206
207 207 return params
208 208
209 209 def get_view(self, *args, **kwargs) -> str:
210 210 """
211 211 Renders post's HTML view. Some of the post params can be passed over
212 212 kwargs for the means of caching (if we view the thread, some params
213 213 are same for every post and don't need to be computed over and over.
214 214 """
215 params = self.get_view_params(args, kwargs)
215 params = self.get_view_params(*args, **kwargs)
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) -> Attachment:
223 223 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE).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 try:
349 349 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
350 350 replacements[post_id] = absolute_post_id
351 351 except Post.DoesNotExist:
352 352 pass
353 353
354 354 text = self.get_raw_text() or ''
355 355 for key in replacements:
356 356 text = text.replace('[post]{}[/post]'.format(key),
357 357 '[post]{}[/post]'.format(replacements[key]))
358 358 text = text.replace('\r\n', '\n').replace('\r', '\n')
359 359
360 360 return text
361 361
362 362 def connect_threads(self, opening_posts):
363 363 for opening_post in opening_posts:
364 364 threads = opening_post.get_threads().all()
365 365 for thread in threads:
366 366 if thread.can_bump():
367 367 thread.update_bump_status()
368 368
369 369 thread.last_edit_time = self.last_edit_time
370 370 thread.save(update_fields=['last_edit_time', 'status'])
371 371 self.threads.add(opening_post.get_thread())
372 372
373 373 def get_tripcode(self):
374 374 if self.tripcode:
375 375 return Tripcode(self.tripcode)
376 376
377 377 def get_link_view(self):
378 378 """
379 379 Gets view of a reflink to the post.
380 380 """
381 381 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
382 382 self.id)
383 383 if self.is_opening():
384 384 result = '<b>{}</b>'.format(result)
385 385
386 386 return result
387 387
388 388 def is_hidden(self) -> bool:
389 389 return self.hidden
390 390
391 391 def set_hidden(self, hidden):
392 392 self.hidden = hidden
393 393
394 394 def increment_version(self):
395 395 self.version = F('version') + 1
396 396
397 397 def clear_cache(self):
398 398 """
399 399 Clears sync data (content cache, signatures etc).
400 400 """
401 401 global_id = self.global_id
402 402 if global_id is not None and global_id.is_local()\
403 403 and global_id.content is not None:
404 404 global_id.clear_cache()
405 405
406 406 def get_tags(self):
407 407 return self.get_thread().get_tags()
408 408
409 409 def get_ip_color(self):
410 410 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
@@ -1,50 +1,51 b''
1 1 import re
2 2 from django.shortcuts import get_object_or_404
3 3 from django import template
4 4
5 5
6 6 IMG_ACTION_URL = '[<a href="{}">{}</a>]'
7 7
8 8
9 9 register = template.Library()
10 10
11 11 actions = [
12 12 {
13 13 'name': 'google',
14 14 'link': 'http://google.com/searchbyimage?image_url=%s',
15 15 },
16 16 {
17 17 'name': 'iqdb',
18 18 'link': 'http://iqdb.org/?url=%s',
19 19 },
20 20 ]
21 21
22 22
23 23 @register.simple_tag(name='post_url')
24 24 def post_url(*args, **kwargs):
25 25 post_id = args[0]
26 26
27 27 post = get_object_or_404('Post', id=post_id)
28 28
29 29 return post.get_absolute_url()
30 30
31 31
32 32 @register.simple_tag(name='image_actions')
33 33 def image_actions(*args, **kwargs):
34 34 image_link = args[0]
35 35 if len(args) > 1:
36 36 image_link = 'http://' + args[1] + image_link # TODO https?
37 37
38 38 return ', '.join([IMG_ACTION_URL.format(
39 39 action['link'] % image_link, action['name']) for action in actions])
40 40
41 41
42 42 @register.inclusion_tag('boards/post.html', name='post_view', takes_context=True)
43 43 def post_view(context, post, *args, **kwargs):
44 44 kwargs['perms'] = context['perms']
45 45 return post.get_view_params(*args, **kwargs)
46 46
47
47 48 @register.simple_tag(name='page_url')
48 49 def page_url(paginator, page_number, *args, **kwargs):
49 50 if paginator.supports_urls():
50 51 return paginator.get_page_url(page_number)
General Comments 0
You need to be logged in to leave comments. Login now