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