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