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