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