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