##// END OF EJS Templates
Do not show IP colored lines and feed selectors when there is no saved IP
neko259 -
r1678:a45f98f4 default
parent child Browse files
Show More
@@ -1,405 +1,409 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 from boards.models.post.manager import PostManager
11 from boards.models.post.manager import PostManager, NO_IP
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(db_index=True)
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 is cached only for the "main" thread. When getting url
148 148 # for other threads, do it manually.
149 149 return self.url
150 150
151 151 def get_thread(self):
152 152 return self.thread
153 153
154 154 def get_thread_id(self):
155 155 return self.thread_id
156 156
157 157 def get_threads(self) -> QuerySet:
158 158 """
159 159 Gets post's thread.
160 160 """
161 161
162 162 return self.threads
163 163
164 164 def _get_cache_key(self):
165 165 return [datetime_to_epoch(self.last_edit_time)]
166 166
167 167 def get_view_params(self, *args, **kwargs):
168 168 """
169 169 Gets the parameters required for viewing the post based on the arguments
170 170 given and the post itself.
171 171 """
172 172 thread = kwargs.get('thread') or self.get_thread()
173 173
174 174 css_classes = [CSS_CLS_POST]
175 175 if thread.is_archived():
176 176 css_classes.append(CSS_CLS_ARCHIVE_POST)
177 177 elif not thread.can_bump():
178 178 css_classes.append(CSS_CLS_DEAD_POST)
179 179 if self.is_hidden():
180 180 css_classes.append(CSS_CLS_HIDDEN_POST)
181 181 if thread.is_monochrome():
182 182 css_classes.append(CSS_CLS_MONOCHROME)
183 183
184 184 params = dict()
185 185 for param in POST_VIEW_PARAMS:
186 186 if param in kwargs:
187 187 params[param] = kwargs[param]
188 188
189 189 params.update({
190 190 PARAMETER_POST: self,
191 191 PARAMETER_IS_OPENING: self.is_opening(),
192 192 PARAMETER_THREAD: thread,
193 193 PARAMETER_CSS_CLASS: ' '.join(css_classes),
194 194 })
195 195
196 196 return params
197 197
198 198 def get_view(self, *args, **kwargs) -> str:
199 199 """
200 200 Renders post's HTML view. Some of the post params can be passed over
201 201 kwargs for the means of caching (if we view the thread, some params
202 202 are same for every post and don't need to be computed over and over.
203 203 """
204 204 params = self.get_view_params(*args, **kwargs)
205 205
206 206 return render_to_string('boards/post.html', params)
207 207
208 208 def get_search_view(self, *args, **kwargs):
209 209 return self.get_view(need_op_data=True, *args, **kwargs)
210 210
211 211 def get_first_image(self) -> Attachment:
212 212 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE).earliest('id')
213 213
214 214 def set_global_id(self, key_pair=None):
215 215 """
216 216 Sets global id based on the given key pair. If no key pair is given,
217 217 default one is used.
218 218 """
219 219
220 220 if key_pair:
221 221 key = key_pair
222 222 else:
223 223 try:
224 224 key = KeyPair.objects.get(primary=True)
225 225 except KeyPair.DoesNotExist:
226 226 # Do not update the global id because there is no key defined
227 227 return
228 228 global_id = GlobalId(key_type=key.key_type,
229 229 key=key.public_key,
230 230 local_id=self.id)
231 231 global_id.save()
232 232
233 233 self.global_id = global_id
234 234
235 235 self.save(update_fields=['global_id'])
236 236
237 237 def get_pub_time_str(self):
238 238 return str(self.pub_time)
239 239
240 240 def get_replied_ids(self):
241 241 """
242 242 Gets ID list of the posts that this post replies.
243 243 """
244 244
245 245 raw_text = self.get_raw_text()
246 246
247 247 local_replied = REGEX_REPLY.findall(raw_text)
248 248 global_replied = []
249 249 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
250 250 key_type = match[0]
251 251 key = match[1]
252 252 local_id = match[2]
253 253
254 254 try:
255 255 global_id = GlobalId.objects.get(key_type=key_type,
256 256 key=key, local_id=local_id)
257 257 for post in Post.objects.filter(global_id=global_id).only('id'):
258 258 global_replied.append(post.id)
259 259 except GlobalId.DoesNotExist:
260 260 pass
261 261 return local_replied + global_replied
262 262
263 263 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
264 264 include_last_update=False) -> str:
265 265 """
266 266 Gets post HTML or JSON data that can be rendered on a page or used by
267 267 API.
268 268 """
269 269
270 270 return get_exporter(format_type).export(self, request,
271 271 include_last_update)
272 272
273 273 def notify_clients(self, recursive=True):
274 274 """
275 275 Sends post HTML data to the thread web socket.
276 276 """
277 277
278 278 if not settings.get_bool('External', 'WebsocketsEnabled'):
279 279 return
280 280
281 281 thread_ids = list()
282 282 for thread in self.get_threads().all():
283 283 thread_ids.append(thread.id)
284 284
285 285 thread.notify_clients()
286 286
287 287 if recursive:
288 288 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
289 289 post_id = reply_number.group(1)
290 290
291 291 try:
292 292 ref_post = Post.objects.get(id=post_id)
293 293
294 294 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
295 295 # If post is in this thread, its thread was already notified.
296 296 # Otherwise, notify its thread separately.
297 297 ref_post.notify_clients(recursive=False)
298 298 except ObjectDoesNotExist:
299 299 pass
300 300
301 301 def _build_url(self):
302 302 opening = self.is_opening()
303 303 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
304 304 url = reverse('thread', kwargs={'post_id': opening_id})
305 305 if not opening:
306 306 url += '#' + str(self.id)
307 307
308 308 return url
309 309
310 310 def save(self, force_insert=False, force_update=False, using=None,
311 311 update_fields=None):
312 312 new_post = self.id is None
313 313
314 314 self.uid = str(uuid.uuid4())
315 315 if update_fields is not None and 'uid' not in update_fields:
316 316 update_fields += ['uid']
317 317
318 318 if not new_post:
319 319 for thread in self.get_threads().all():
320 320 thread.last_edit_time = self.last_edit_time
321 321
322 322 thread.save(update_fields=['last_edit_time', 'status'])
323 323
324 324 super().save(force_insert, force_update, using, update_fields)
325 325
326 326 if new_post:
327 327 self.url = self._build_url()
328 328 super().save(update_fields=['url'])
329 329
330 330 def get_text(self) -> str:
331 331 return self._text_rendered
332 332
333 333 def get_raw_text(self) -> str:
334 334 return self.text
335 335
336 336 def get_sync_text(self) -> str:
337 337 """
338 338 Returns text applicable for sync. It has absolute post reflinks.
339 339 """
340 340
341 341 replacements = dict()
342 342 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
343 343 try:
344 344 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
345 345 replacements[post_id] = absolute_post_id
346 346 except Post.DoesNotExist:
347 347 pass
348 348
349 349 text = self.get_raw_text() or ''
350 350 for key in replacements:
351 351 text = text.replace('[post]{}[/post]'.format(key),
352 352 '[post]{}[/post]'.format(replacements[key]))
353 353 text = text.replace('\r\n', '\n').replace('\r', '\n')
354 354
355 355 return text
356 356
357 357 def connect_threads(self, opening_posts):
358 358 for opening_post in opening_posts:
359 359 threads = opening_post.get_threads().all()
360 360 for thread in threads:
361 361 if thread.can_bump():
362 362 thread.update_bump_status()
363 363
364 364 thread.last_edit_time = self.last_edit_time
365 365 thread.save(update_fields=['last_edit_time', 'status'])
366 366 self.threads.add(opening_post.get_thread())
367 367
368 368 def get_tripcode(self):
369 369 if self.tripcode:
370 370 return Tripcode(self.tripcode)
371 371
372 372 def get_link_view(self):
373 373 """
374 374 Gets view of a reflink to the post.
375 375 """
376 376 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
377 377 self.id)
378 378 if self.is_opening():
379 379 result = '<b>{}</b>'.format(result)
380 380
381 381 return result
382 382
383 383 def is_hidden(self) -> bool:
384 384 return self.hidden
385 385
386 386 def set_hidden(self, hidden):
387 387 self.hidden = hidden
388 388
389 389 def increment_version(self):
390 390 self.version = F('version') + 1
391 391
392 392 def clear_cache(self):
393 393 """
394 394 Clears sync data (content cache, signatures etc).
395 395 """
396 396 global_id = self.global_id
397 397 if global_id is not None and global_id.is_local()\
398 398 and global_id.content is not None:
399 399 global_id.clear_cache()
400 400
401 401 def get_tags(self):
402 402 return self.get_thread().get_tags()
403 403
404 404 def get_ip_color(self):
405 405 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
406
407 def has_ip(self):
408 return self.poster_ip != NO_IP
409
@@ -1,110 +1,110 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 8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.id }}</a>
9 9 <span class="title">{{ post.title }}</span>
10 {% if perms.boards.change_post %}
10 {% if perms.boards.change_post and post.has_ip %}
11 11 <a href="{% url 'feed' %}?ip={{ post.poster_ip }}">*</a>
12 12 <span class="pub_time" style="border-bottom: solid 2px #{{ post.get_ip_color }};" title="{{ post.poster_ip }}">
13 13 {% else %}
14 14 <span class="pub_time">
15 15 {% endif %}
16 16 <time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
17 17 {% if post.tripcode %}
18 18 /
19 19 {% with tripcode=post.get_tripcode %}
20 20 <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}"
21 21 class="tripcode" title="{{ tripcode.get_full_text }}"
22 22 style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a>
23 23 {% endwith %}
24 24 {% endif %}
25 25 {% comment %}
26 26 Thread death time needs to be shown only if the thread is alredy archived
27 27 and this is an opening post (thread death time) or a post for popup
28 28 (we don't see OP here so we show the death time in the post itself).
29 29 {% endcomment %}
30 30 {% if is_opening and thread.is_archived %}
31 31 <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
32 32 {% endif %}
33 33 {% if is_opening %}
34 34 {% if need_open_link %}
35 35 {% if thread.is_archived %}
36 36 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
37 37 {% else %}
38 38 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
39 39 {% endif %}
40 40 {% endif %}
41 41 {% else %}
42 42 {% if need_op_data %}
43 43 {% with thread.get_opening_post as op %}
44 44 {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title_or_text }}</span>
45 45 {% endwith %}
46 46 {% endif %}
47 47 {% endif %}
48 48 {% if reply_link and not thread.is_archived %}
49 49 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
50 50 {% endif %}
51 51
52 52 {% if perms.boards.change_post or perms.boards.delete_post or perms.boards.change_thread or perms_boards.delete_thread %}
53 53 <span class="moderator_info">
54 54 {% if perms.boards.change_post or perms.boards.delete_post %}
55 55 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
56 56 {% endif %}
57 57 {% if perms.boards.change_thread or perms_boards.delete_thread %}
58 58 {% if is_opening %}
59 59 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
60 60 | <a href="{% url 'admin:boards_thread_delete' thread.id %}">{% trans 'Delete thread' %}</a>
61 61 {% else %}
62 62 | <a href="{% url 'admin:boards_post_delete' post.id %}">{% trans 'Delete post' %}</a>
63 63 {% endif %}
64 64 {% endif %}
65 65 {% if post.global_id_id %}
66 66 | <a href="{% url 'post_sync_data' post.id %}">RAW</a>
67 67 {% endif %}
68 68 </span>
69 69 {% endif %}
70 70 </div>
71 71 {% comment %}
72 72 Post images. Currently only 1 image can be posted and shown, but post model
73 73 supports multiple.
74 74 {% endcomment %}
75 75 {% for image in post.images.all %}
76 76 {{ image.get_view|safe }}
77 77 {% endfor %}
78 78 {% for file in post.attachments.all %}
79 79 {{ file.get_view|safe }}
80 80 {% endfor %}
81 81 {% comment %}
82 82 Post message (text)
83 83 {% endcomment %}
84 84 <div class="message">
85 85 {% if truncated %}
86 86 {{ post.get_text|truncatewords_html:50|safe }}
87 87 {% else %}
88 88 {{ post.get_text|safe }}
89 89 {% endif %}
90 90 </div>
91 91 {% if post.is_referenced and not mode_tree %}
92 92 <div class="refmap">
93 93 {% trans "Replies" %}: {{ post.refmap|safe }}
94 94 </div>
95 95 {% endif %}
96 96 {% comment %}
97 97 Thread metadata: counters, tags etc
98 98 {% endcomment %}
99 99 {% if is_opening %}
100 100 <div class="metadata">
101 101 {% if need_open_link %}
102 102 ♥ {{ thread.get_reply_count }}
103 103 ❄ {{ thread.get_images_count }}
104 104 {% endif %}
105 105 <span class="tags">
106 106 {{ thread.get_tag_url_list|safe }}
107 107 </span>
108 108 </div>
109 109 {% endif %}
110 110 </div>
General Comments 0
You need to be logged in to leave comments. Login now