##// END OF EJS Templates
Do not pass over perms variable manually to the post template. Fixed thread...
neko259 -
r1390:1252678f default
parent child Browse files
Show More
@@ -1,361 +1,360 b''
1 import logging
1 import logging
2 import re
2 import re
3 import uuid
3 import uuid
4
4
5 from django.core.exceptions import ObjectDoesNotExist
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.urlresolvers import reverse
6 from django.core.urlresolvers import reverse
7 from django.db import models
7 from django.db import models
8 from django.db.models import TextField, QuerySet
8 from django.db.models import TextField, QuerySet
9 from django.template.defaultfilters import striptags, truncatewords
9 from django.template.defaultfilters import striptags, truncatewords
10 from django.template.loader import render_to_string
10 from django.template.loader import render_to_string
11 from django.utils import timezone
11 from django.utils import timezone
12
12
13 from boards import settings
13 from boards import settings
14 from boards.abstracts.tripcode import Tripcode
14 from boards.abstracts.tripcode import Tripcode
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage, Attachment
16 from boards.models import PostImage, Attachment
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
18 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
19 from boards.models.post.manager import PostManager
19 from boards.models.post.manager import PostManager
20 from boards.models.user import Notification
20 from boards.models.user import Notification
21
21
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
22 CSS_CLS_HIDDEN_POST = 'hidden_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
23 CSS_CLS_DEAD_POST = 'dead_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
24 CSS_CLS_ARCHIVE_POST = 'archive_post'
25 CSS_CLS_POST = 'post'
25 CSS_CLS_POST = 'post'
26
26
27 TITLE_MAX_WORDS = 10
27 TITLE_MAX_WORDS = 10
28
28
29 APP_LABEL_BOARDS = 'boards'
29 APP_LABEL_BOARDS = 'boards'
30
30
31 BAN_REASON_AUTO = 'Auto'
31 BAN_REASON_AUTO = 'Auto'
32
32
33 IMAGE_THUMB_SIZE = (200, 150)
33 IMAGE_THUMB_SIZE = (200, 150)
34
34
35 TITLE_MAX_LENGTH = 200
35 TITLE_MAX_LENGTH = 200
36
36
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
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_MODERATOR = 'moderator'
48 PARAMETER_POST = 'post'
47 PARAMETER_POST = 'post'
49 PARAMETER_OP_ID = 'opening_post_id'
48 PARAMETER_OP_ID = 'opening_post_id'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
50 PARAMETER_REPLY_LINK = 'reply_link'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
51 PARAMETER_NEED_OP_DATA = 'need_op_data'
53
52
54 POST_VIEW_PARAMS = (
53 POST_VIEW_PARAMS = (
55 'need_op_data',
54 'need_op_data',
56 'reply_link',
55 'reply_link',
57 'need_open_link',
56 'need_open_link',
58 'truncated',
57 'truncated',
59 'mode_tree',
58 'mode_tree',
60 'perms',
59 'perms',
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 tripcode = models.CharField(max_length=50, blank=True, default='')
99 tripcode = models.CharField(max_length=50, blank=True, default='')
101 opening = models.BooleanField(db_index=True)
100 opening = models.BooleanField(db_index=True)
102 hidden = models.BooleanField(default=False)
101 hidden = models.BooleanField(default=False)
103
102
104 def __str__(self):
103 def __str__(self):
105 return 'P#{}/{}'.format(self.id, self.get_title())
104 return 'P#{}/{}'.format(self.id, self.get_title())
106
105
107 def get_referenced_posts(self):
106 def get_referenced_posts(self):
108 threads = self.get_threads().all()
107 threads = self.get_threads().all()
109 return self.referenced_posts.filter(threads__in=threads)\
108 return self.referenced_posts.filter(threads__in=threads)\
110 .order_by('pub_time').distinct().all()
109 .order_by('pub_time').distinct().all()
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_id = thread.get_opening_post_id()
154 opening_id = thread.get_opening_post_id()
156 url = reverse('thread', kwargs={'post_id': opening_id})
155 url = reverse('thread', kwargs={'post_id': opening_id})
157 if self.id != opening_id:
156 if self.id != opening_id:
158 url += '#' + str(self.id)
157 url += '#' + str(self.id)
159
158
160 return url
159 return url
161
160
162 def get_thread(self):
161 def get_thread(self):
163 return self.thread
162 return self.thread
164
163
165 def get_threads(self) -> QuerySet:
164 def get_threads(self) -> QuerySet:
166 """
165 """
167 Gets post's thread.
166 Gets post's thread.
168 """
167 """
169
168
170 return self.threads
169 return self.threads
171
170
172 def get_view(self, *args, **kwargs) -> str:
171 def get_view(self, *args, **kwargs) -> str:
173 """
172 """
174 Renders post's HTML view. Some of the post params can be passed over
173 Renders post's HTML view. Some of the post params can be passed over
175 kwargs for the means of caching (if we view the thread, some params
174 kwargs for the means of caching (if we view the thread, some params
176 are same for every post and don't need to be computed over and over.
175 are same for every post and don't need to be computed over and over.
177 """
176 """
178
177
179 thread = self.get_thread()
178 thread = self.get_thread()
180
179
181 css_classes = [CSS_CLS_POST]
180 css_classes = [CSS_CLS_POST]
182 if thread.archived:
181 if thread.archived:
183 css_classes.append(CSS_CLS_ARCHIVE_POST)
182 css_classes.append(CSS_CLS_ARCHIVE_POST)
184 elif not thread.can_bump():
183 elif not thread.can_bump():
185 css_classes.append(CSS_CLS_DEAD_POST)
184 css_classes.append(CSS_CLS_DEAD_POST)
186 if self.is_hidden():
185 if self.is_hidden():
187 css_classes.append(CSS_CLS_HIDDEN_POST)
186 css_classes.append(CSS_CLS_HIDDEN_POST)
188
187
189 params = dict()
188 params = dict()
190 for param in POST_VIEW_PARAMS:
189 for param in POST_VIEW_PARAMS:
191 if param in kwargs:
190 if param in kwargs:
192 params[param] = kwargs[param]
191 params[param] = kwargs[param]
193
192
194 params.update({
193 params.update({
195 PARAMETER_POST: self,
194 PARAMETER_POST: self,
196 PARAMETER_IS_OPENING: self.is_opening(),
195 PARAMETER_IS_OPENING: self.is_opening(),
197 PARAMETER_THREAD: thread,
196 PARAMETER_THREAD: thread,
198 PARAMETER_CSS_CLASS: ' '.join(css_classes),
197 PARAMETER_CSS_CLASS: ' '.join(css_classes),
199 })
198 })
200
199
201 return render_to_string('boards/post.html', params)
200 return render_to_string('boards/post.html', params)
202
201
203 def get_search_view(self, *args, **kwargs):
202 def get_search_view(self, *args, **kwargs):
204 return self.get_view(need_op_data=True, *args, **kwargs)
203 return self.get_view(need_op_data=True, *args, **kwargs)
205
204
206 def get_first_image(self) -> PostImage:
205 def get_first_image(self) -> PostImage:
207 return self.images.earliest('id')
206 return self.images.earliest('id')
208
207
209 def delete(self, using=None):
208 def delete(self, using=None):
210 """
209 """
211 Deletes all post images and the post itself.
210 Deletes all post images and the post itself.
212 """
211 """
213
212
214 for image in self.images.all():
213 for image in self.images.all():
215 image_refs_count = image.post_images.count()
214 image_refs_count = image.post_images.count()
216 if image_refs_count == 1:
215 if image_refs_count == 1:
217 image.delete()
216 image.delete()
218
217
219 for attachment in self.attachments.all():
218 for attachment in self.attachments.all():
220 attachment_refs_count = attachment.attachment_posts.count()
219 attachment_refs_count = attachment.attachment_posts.count()
221 if attachment_refs_count == 1:
220 if attachment_refs_count == 1:
222 attachment.delete()
221 attachment.delete()
223
222
224 thread = self.get_thread()
223 thread = self.get_thread()
225 thread.last_edit_time = timezone.now()
224 thread.last_edit_time = timezone.now()
226 thread.save()
225 thread.save()
227
226
228 super(Post, self).delete(using)
227 super(Post, self).delete(using)
229
228
230 logging.getLogger('boards.post.delete').info(
229 logging.getLogger('boards.post.delete').info(
231 'Deleted post {}'.format(self))
230 'Deleted post {}'.format(self))
232
231
233 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
232 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
234 include_last_update=False) -> str:
233 include_last_update=False) -> str:
235 """
234 """
236 Gets post HTML or JSON data that can be rendered on a page or used by
235 Gets post HTML or JSON data that can be rendered on a page or used by
237 API.
236 API.
238 """
237 """
239
238
240 return get_exporter(format_type).export(self, request,
239 return get_exporter(format_type).export(self, request,
241 include_last_update)
240 include_last_update)
242
241
243 def notify_clients(self, recursive=True):
242 def notify_clients(self, recursive=True):
244 """
243 """
245 Sends post HTML data to the thread web socket.
244 Sends post HTML data to the thread web socket.
246 """
245 """
247
246
248 if not settings.get_bool('External', 'WebsocketsEnabled'):
247 if not settings.get_bool('External', 'WebsocketsEnabled'):
249 return
248 return
250
249
251 thread_ids = list()
250 thread_ids = list()
252 for thread in self.get_threads().all():
251 for thread in self.get_threads().all():
253 thread_ids.append(thread.id)
252 thread_ids.append(thread.id)
254
253
255 thread.notify_clients()
254 thread.notify_clients()
256
255
257 if recursive:
256 if recursive:
258 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
257 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
259 post_id = reply_number.group(1)
258 post_id = reply_number.group(1)
260
259
261 try:
260 try:
262 ref_post = Post.objects.get(id=post_id)
261 ref_post = Post.objects.get(id=post_id)
263
262
264 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
263 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
265 # If post is in this thread, its thread was already notified.
264 # If post is in this thread, its thread was already notified.
266 # Otherwise, notify its thread separately.
265 # Otherwise, notify its thread separately.
267 ref_post.notify_clients(recursive=False)
266 ref_post.notify_clients(recursive=False)
268 except ObjectDoesNotExist:
267 except ObjectDoesNotExist:
269 pass
268 pass
270
269
271 def build_url(self):
270 def build_url(self):
272 self.url = self.get_absolute_url()
271 self.url = self.get_absolute_url()
273 self.save(update_fields=['url'])
272 self.save(update_fields=['url'])
274
273
275 def save(self, force_insert=False, force_update=False, using=None,
274 def save(self, force_insert=False, force_update=False, using=None,
276 update_fields=None):
275 update_fields=None):
277 self._text_rendered = Parser().parse(self.get_raw_text())
276 self._text_rendered = Parser().parse(self.get_raw_text())
278
277
279 self.uid = str(uuid.uuid4())
278 self.uid = str(uuid.uuid4())
280 if update_fields is not None and 'uid' not in update_fields:
279 if update_fields is not None and 'uid' not in update_fields:
281 update_fields += ['uid']
280 update_fields += ['uid']
282
281
283 if self.id:
282 if self.id:
284 for thread in self.get_threads().all():
283 for thread in self.get_threads().all():
285 thread.last_edit_time = self.last_edit_time
284 thread.last_edit_time = self.last_edit_time
286
285
287 thread.save(update_fields=['last_edit_time', 'bumpable'])
286 thread.save(update_fields=['last_edit_time', 'bumpable'])
288
287
289 super().save(force_insert, force_update, using, update_fields)
288 super().save(force_insert, force_update, using, update_fields)
290
289
291 def get_text(self) -> str:
290 def get_text(self) -> str:
292 return self._text_rendered
291 return self._text_rendered
293
292
294 def get_raw_text(self) -> str:
293 def get_raw_text(self) -> str:
295 return self.text
294 return self.text
296
295
297 def get_absolute_id(self) -> str:
296 def get_absolute_id(self) -> str:
298 """
297 """
299 If the post has many threads, shows its main thread OP id in the post
298 If the post has many threads, shows its main thread OP id in the post
300 ID.
299 ID.
301 """
300 """
302
301
303 if self.get_threads().count() > 1:
302 if self.get_threads().count() > 1:
304 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
303 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
305 else:
304 else:
306 return str(self.id)
305 return str(self.id)
307
306
308 def connect_notifications(self):
307 def connect_notifications(self):
309 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
308 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
310 user_name = reply_number.group(1).lower()
309 user_name = reply_number.group(1).lower()
311 Notification.objects.get_or_create(name=user_name, post=self)
310 Notification.objects.get_or_create(name=user_name, post=self)
312
311
313 def connect_replies(self):
312 def connect_replies(self):
314 """
313 """
315 Connects replies to a post to show them as a reflink map
314 Connects replies to a post to show them as a reflink map
316 """
315 """
317
316
318 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
317 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
319 post_id = reply_number.group(1)
318 post_id = reply_number.group(1)
320
319
321 try:
320 try:
322 referenced_post = Post.objects.get(id=post_id)
321 referenced_post = Post.objects.get(id=post_id)
323
322
324 referenced_post.referenced_posts.add(self)
323 referenced_post.referenced_posts.add(self)
325 referenced_post.last_edit_time = self.pub_time
324 referenced_post.last_edit_time = self.pub_time
326 referenced_post.build_refmap()
325 referenced_post.build_refmap()
327 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
326 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
328 except ObjectDoesNotExist:
327 except ObjectDoesNotExist:
329 pass
328 pass
330
329
331 def connect_threads(self, opening_posts):
330 def connect_threads(self, opening_posts):
332 for opening_post in opening_posts:
331 for opening_post in opening_posts:
333 threads = opening_post.get_threads().all()
332 threads = opening_post.get_threads().all()
334 for thread in threads:
333 for thread in threads:
335 if thread.can_bump():
334 if thread.can_bump():
336 thread.update_bump_status()
335 thread.update_bump_status()
337
336
338 thread.last_edit_time = self.last_edit_time
337 thread.last_edit_time = self.last_edit_time
339 thread.save(update_fields=['last_edit_time', 'bumpable'])
338 thread.save(update_fields=['last_edit_time', 'bumpable'])
340 self.threads.add(opening_post.get_thread())
339 self.threads.add(opening_post.get_thread())
341
340
342 def get_tripcode(self):
341 def get_tripcode(self):
343 if self.tripcode:
342 if self.tripcode:
344 return Tripcode(self.tripcode)
343 return Tripcode(self.tripcode)
345
344
346 def get_link_view(self):
345 def get_link_view(self):
347 """
346 """
348 Gets view of a reflink to the post.
347 Gets view of a reflink to the post.
349 """
348 """
350 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
349 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
351 self.id)
350 self.id)
352 if self.is_opening():
351 if self.is_opening():
353 result = '<b>{}</b>'.format(result)
352 result = '<b>{}</b>'.format(result)
354
353
355 return result
354 return result
356
355
357 def is_hidden(self) -> bool:
356 def is_hidden(self) -> bool:
358 return self.hidden
357 return self.hidden
359
358
360 def set_hidden(self, hidden):
359 def set_hidden(self, hidden):
361 self.hidden = hidden
360 self.hidden = hidden
@@ -1,55 +1,57 b''
1 from django.contrib.auth.context_processors import PermWrapper
2
1 from boards import utils
3 from boards import utils
2
4
3
5
4 PARAMETER_TRUNCATED = 'truncated'
6 PARAMETER_TRUNCATED = 'truncated'
5
7
6 DIFF_TYPE_HTML = 'html'
8 DIFF_TYPE_HTML = 'html'
7 DIFF_TYPE_JSON = 'json'
9 DIFF_TYPE_JSON = 'json'
8
10
9
11
10 class Exporter():
12 class Exporter():
11 @staticmethod
13 @staticmethod
12 def export(post, request, include_last_update) -> str:
14 def export(post, request, include_last_update) -> str:
13 pass
15 pass
14
16
15
17
16 class HtmlExporter(Exporter):
18 class HtmlExporter(Exporter):
17 @staticmethod
19 @staticmethod
18 def export(post, request, include_last_update):
20 def export(post, request, include_last_update):
19 if request is not None and PARAMETER_TRUNCATED in request.GET:
21 if request is not None and PARAMETER_TRUNCATED in request.GET:
20 truncated = True
22 truncated = True
21 reply_link = False
23 reply_link = False
22 else:
24 else:
23 truncated = False
25 truncated = False
24 reply_link = True
26 reply_link = True
25
27
26 return post.get_view(truncated=truncated, reply_link=reply_link,
28 return post.get_view(truncated=truncated, reply_link=reply_link,
27 moderator=utils.is_moderator(request))
29 perms=PermWrapper(request.user))
28
30
29
31
30 class JsonExporter(Exporter):
32 class JsonExporter(Exporter):
31 @staticmethod
33 @staticmethod
32 def export(post, request, include_last_update):
34 def export(post, request, include_last_update):
33 post_json = {
35 post_json = {
34 'id': post.id,
36 'id': post.id,
35 'title': post.title,
37 'title': post.title,
36 'text': post.get_raw_text(),
38 'text': post.get_raw_text(),
37 }
39 }
38 if post.images.exists():
40 if post.images.exists():
39 post_image = post.get_first_image()
41 post_image = post.get_first_image()
40 post_json['image'] = post_image.image.url
42 post_json['image'] = post_image.image.url
41 post_json['image_preview'] = post_image.image.url_200x150
43 post_json['image_preview'] = post_image.image.url_200x150
42 if include_last_update:
44 if include_last_update:
43 post_json['bump_time'] = utils.datetime_to_epoch(
45 post_json['bump_time'] = utils.datetime_to_epoch(
44 post.get_thread().bump_time)
46 post.get_thread().bump_time)
45 return post_json
47 return post_json
46
48
47
49
48 EXPORTERS = {
50 EXPORTERS = {
49 DIFF_TYPE_HTML: HtmlExporter,
51 DIFF_TYPE_HTML: HtmlExporter,
50 DIFF_TYPE_JSON: JsonExporter,
52 DIFF_TYPE_JSON: JsonExporter,
51 }
53 }
52
54
53
55
54 def get_exporter(export_type: str) -> Exporter:
56 def get_exporter(export_type: str) -> Exporter:
55 return EXPORTERS[export_type]()
57 return EXPORTERS[export_type]()
@@ -1,186 +1,186 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load board %}
4 {% load board %}
5 {% load static %}
5 {% load static %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block head %}
8 {% block head %}
9 <meta name="robots" content="noindex">
9 <meta name="robots" content="noindex">
10
10
11 {% if tag %}
11 {% if tag %}
12 <title>{{ tag.name }} - {{ site_name }}</title>
12 <title>{{ tag.name }} - {{ site_name }}</title>
13 {% else %}
13 {% else %}
14 <title>{{ site_name }}</title>
14 <title>{{ site_name }}</title>
15 {% endif %}
15 {% endif %}
16
16
17 {% if prev_page_link %}
17 {% if prev_page_link %}
18 <link rel="prev" href="{{ prev_page_link }}" />
18 <link rel="prev" href="{{ prev_page_link }}" />
19 {% endif %}
19 {% endif %}
20 {% if next_page_link %}
20 {% if next_page_link %}
21 <link rel="next" href="{{ next_page_link }}" />
21 <link rel="next" href="{{ next_page_link }}" />
22 {% endif %}
22 {% endif %}
23
23
24 {% endblock %}
24 {% endblock %}
25
25
26 {% block content %}
26 {% block content %}
27
27
28 {% get_current_language as LANGUAGE_CODE %}
28 {% get_current_language as LANGUAGE_CODE %}
29 {% get_current_timezone as TIME_ZONE %}
29 {% get_current_timezone as TIME_ZONE %}
30
30
31 {% for banner in banners %}
31 {% for banner in banners %}
32 <div class="post">
32 <div class="post">
33 <div class="title">{{ banner.title }}</div>
33 <div class="title">{{ banner.title }}</div>
34 <div>{{ banner.text }}</div>
34 <div>{{ banner.text }}</div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 </div>
36 </div>
37 {% endfor %}
37 {% endfor %}
38
38
39 {% if tag %}
39 {% if tag %}
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
41 {% if random_image_post %}
41 {% if random_image_post %}
42 <div class="tag-image">
42 <div class="tag-image">
43 {% with image=random_image_post.images.first %}
43 {% with image=random_image_post.images.first %}
44 <a href="{{ random_image_post.get_absolute_url }}"><img
44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 src="{{ image.image.url_200x150 }}"
45 src="{{ image.image.url_200x150 }}"
46 width="{{ image.pre_width }}"
46 width="{{ image.pre_width }}"
47 height="{{ image.pre_height }}"/></a>
47 height="{{ image.pre_height }}"/></a>
48 {% endwith %}
48 {% endwith %}
49 </div>
49 </div>
50 {% endif %}
50 {% endif %}
51 <div class="tag-text-data">
51 <div class="tag-text-data">
52 <h2>
52 <h2>
53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
54 {% if is_favorite %}
54 {% if is_favorite %}
55 <button name="method" value="unsubscribe" class="fav">β˜…</button>
55 <button name="method" value="unsubscribe" class="fav">β˜…</button>
56 {% else %}
56 {% else %}
57 <button name="method" value="subscribe" class="not_fav">β˜…</button>
57 <button name="method" value="subscribe" class="not_fav">β˜…</button>
58 {% endif %}
58 {% endif %}
59 </form>
59 </form>
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
61 {% if is_hidden %}
61 {% if is_hidden %}
62 <button name="method" value="unhide" class="fav">H</button>
62 <button name="method" value="unhide" class="fav">H</button>
63 {% else %}
63 {% else %}
64 <button name="method" value="hide" class="not_fav">H</button>
64 <button name="method" value="hide" class="not_fav">H</button>
65 {% endif %}
65 {% endif %}
66 </form>
66 </form>
67 {{ tag.get_view|safe }}
67 {{ tag.get_view|safe }}
68 {% if moderator %}
68 {% if moderator %}
69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
70 {% endif %}
70 {% endif %}
71 </h2>
71 </h2>
72 {% if tag.get_description %}
72 {% if tag.get_description %}
73 <p>{{ tag.get_description|safe }}</p>
73 <p>{{ tag.get_description|safe }}</p>
74 {% endif %}
74 {% endif %}
75 <p>
75 <p>
76 {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %},
76 {% blocktrans count count=tag.get_active_thread_count %}{{ count }} active thread{% plural %}active threads{% endblocktrans %},
77 {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %},
77 {% blocktrans count count=tag.get_bumplimit_thread_count %}{{ count }} thread in bumplimit{% plural %} threads in bumplimit{% endblocktrans %},
78 {% blocktrans count count=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
78 {% blocktrans count count=tag.get_archived_thread_count %}{{ count }} archived thread{% plural %}archived threads{% endblocktrans %},
79 {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.
79 {% blocktrans count count=tag.get_post_count %}{{ count }} message{% plural %}messages{% endblocktrans %}.
80 </p>
80 </p>
81 {% if tag.get_all_parents %}
81 {% if tag.get_all_parents %}
82 <p>
82 <p>
83 {% for parent in tag.get_all_parents %}
83 {% for parent in tag.get_all_parents %}
84 {{ parent.get_view|safe }} &gt;
84 {{ parent.get_view|safe }} &gt;
85 {% endfor %}
85 {% endfor %}
86 {{ tag.get_view|safe }}
86 {{ tag.get_view|safe }}
87 </p>
87 </p>
88 {% endif %}
88 {% endif %}
89 </div>
89 </div>
90 </div>
90 </div>
91 {% endif %}
91 {% endif %}
92
92
93 {% if threads %}
93 {% if threads %}
94 {% if prev_page_link %}
94 {% if prev_page_link %}
95 <div class="page_link">
95 <div class="page_link">
96 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
96 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
97 </div>
97 </div>
98 {% endif %}
98 {% endif %}
99
99
100 {% for thread in threads %}
100 {% for thread in threads %}
101 <div class="thread">
101 <div class="thread">
102 {% post_view thread.get_opening_post perms=perms thread=thread truncated=True need_open_link=True %}
102 {% post_view thread.get_opening_post thread=thread truncated=True need_open_link=True %}
103 {% if not thread.archived %}
103 {% if not thread.archived %}
104 {% with last_replies=thread.get_last_replies %}
104 {% with last_replies=thread.get_last_replies %}
105 {% if last_replies %}
105 {% if last_replies %}
106 {% with skipped_replies_count=thread.get_skipped_replies_count %}
106 {% with skipped_replies_count=thread.get_skipped_replies_count %}
107 {% if skipped_replies_count %}
107 {% if skipped_replies_count %}
108 <div class="skipped_replies">
108 <div class="skipped_replies">
109 <a href="{% url 'thread' thread.get_opening_post_id %}">
109 <a href="{% url 'thread' thread.get_opening_post_id %}">
110 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
110 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
111 </a>
111 </a>
112 </div>
112 </div>
113 {% endif %}
113 {% endif %}
114 {% endwith %}
114 {% endwith %}
115 <div class="last-replies">
115 <div class="last-replies">
116 {% for post in last_replies %}
116 {% for post in last_replies %}
117 {% post_view post perms=perms truncated=True %}
117 {% post_view post truncated=True %}
118 {% endfor %}
118 {% endfor %}
119 </div>
119 </div>
120 {% endif %}
120 {% endif %}
121 {% endwith %}
121 {% endwith %}
122 {% endif %}
122 {% endif %}
123 </div>
123 </div>
124 {% endfor %}
124 {% endfor %}
125
125
126 {% if next_page_link %}
126 {% if next_page_link %}
127 <div class="page_link">
127 <div class="page_link">
128 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
128 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
129 </div>
129 </div>
130 {% endif %}
130 {% endif %}
131 {% else %}
131 {% else %}
132 <div class="post">
132 <div class="post">
133 {% trans 'No threads exist. Create the first one!' %}</div>
133 {% trans 'No threads exist. Create the first one!' %}</div>
134 {% endif %}
134 {% endif %}
135
135
136 <div class="post-form-w">
136 <div class="post-form-w">
137 <script src="{% static 'js/panel.js' %}"></script>
137 <script src="{% static 'js/panel.js' %}"></script>
138 <div class="post-form">
138 <div class="post-form">
139 <div class="form-title">{% trans "Create new thread" %}</div>
139 <div class="form-title">{% trans "Create new thread" %}</div>
140 <div class="swappable-form-full">
140 <div class="swappable-form-full">
141 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
141 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
142 {{ form.as_div }}
142 {{ form.as_div }}
143 <div class="form-submit">
143 <div class="form-submit">
144 <input type="submit" value="{% trans "Post" %}"/>
144 <input type="submit" value="{% trans "Post" %}"/>
145 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
145 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
146 </div>
146 </div>
147 </form>
147 </form>
148 </div>
148 </div>
149 <div>
149 <div>
150 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
150 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
151 </div>
151 </div>
152 <div id="preview-text"></div>
152 <div id="preview-text"></div>
153 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
153 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
154 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
154 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
155 </div>
155 </div>
156 </div>
156 </div>
157
157
158 <script src="{% static 'js/form.js' %}"></script>
158 <script src="{% static 'js/form.js' %}"></script>
159 <script src="{% static 'js/thread_create.js' %}"></script>
159 <script src="{% static 'js/thread_create.js' %}"></script>
160
160
161 {% endblock %}
161 {% endblock %}
162
162
163 {% block metapanel %}
163 {% block metapanel %}
164
164
165 <span class="metapanel">
165 <span class="metapanel">
166 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
166 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
167 {% trans "Pages:" %}
167 {% trans "Pages:" %}
168 [
168 [
169 {% with dividers=paginator.get_dividers %}
169 {% with dividers=paginator.get_dividers %}
170 {% for page in paginator.get_divided_range %}
170 {% for page in paginator.get_divided_range %}
171 {% if page in dividers %}
171 {% if page in dividers %}
172 …,
172 …,
173 {% endif %}
173 {% endif %}
174 <a
174 <a
175 {% ifequal page current_page.number %}
175 {% ifequal page current_page.number %}
176 class="current_page"
176 class="current_page"
177 {% endifequal %}
177 {% endifequal %}
178 href="{% page_url paginator page %}">{{ page }}</a>
178 href="{% page_url paginator page %}">{{ page }}</a>
179 {% if not forloop.last %},{% endif %}
179 {% if not forloop.last %},{% endif %}
180 {% endfor %}
180 {% endfor %}
181 {% endwith %}
181 {% endwith %}
182 ]
182 ]
183 [<a href="rss/">RSS</a>]
183 [<a href="rss/">RSS</a>]
184 </span>
184 </span>
185
185
186 {% endblock %}
186 {% endblock %}
@@ -1,70 +1,70 b''
1 {% extends "boards/thread.html" %}
1 {% extends "boards/thread.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5 {% load board %}
5 {% load board %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block thread_content %}
8 {% block thread_content %}
9 {% get_current_language as LANGUAGE_CODE %}
9 {% get_current_language as LANGUAGE_CODE %}
10 {% get_current_timezone as TIME_ZONE %}
10 {% get_current_timezone as TIME_ZONE %}
11
11
12 <div class="tag_info">
12 <div class="tag_info">
13 <h2>
13 <h2>
14 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
14 <form action="{% url 'thread' opening_post.id %}" method="post" class="post-button-form">
15 {% if is_favorite %}
15 {% if is_favorite %}
16 <button name="method" value="unsubscribe" class="fav">β˜…</button>
16 <button name="method" value="unsubscribe" class="fav">β˜…</button>
17 {% else %}
17 {% else %}
18 <button name="method" value="subscribe" class="not_fav">β˜…</button>
18 <button name="method" value="subscribe" class="not_fav">β˜…</button>
19 {% endif %}
19 {% endif %}
20 </form>
20 </form>
21 {{ opening_post.get_title_or_text }}
21 {{ opening_post.get_title_or_text }}
22 </h2>
22 </h2>
23 </div>
23 </div>
24
24
25 {% if bumpable and thread.has_post_limit %}
25 {% if bumpable and thread.has_post_limit %}
26 <div class="bar-bg">
26 <div class="bar-bg">
27 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
27 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
28 </div>
28 </div>
29 <div class="bar-text">
29 <div class="bar-text">
30 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
30 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
31 </div>
31 </div>
32 </div>
32 </div>
33 {% endif %}
33 {% endif %}
34
34
35 <div class="thread">
35 <div class="thread">
36 {% for post in thread.get_replies %}
36 {% for post in thread.get_replies %}
37 {% post_view post perms=perms reply_link=True %}
37 {% post_view post reply_link=True %}
38 {% endfor %}
38 {% endfor %}
39 </div>
39 </div>
40
40
41 {% if not thread.archived %}
41 {% if not thread.archived %}
42 <div class="post-form-w">
42 <div class="post-form-w">
43 <script src="{% static 'js/panel.js' %}"></script>
43 <script src="{% static 'js/panel.js' %}"></script>
44 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
44 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}<span class="reply-to-message"> {% trans "to message " %} #<span id="reply-to-message-id"></span></span></div>
45 <div class="post-form" id="compact-form">
45 <div class="post-form" id="compact-form">
46 <div class="swappable-form-full">
46 <div class="swappable-form-full">
47 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
47 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
48 <div class="compact-form-text"></div>
48 <div class="compact-form-text"></div>
49 {{ form.as_div }}
49 {{ form.as_div }}
50 <div class="form-submit">
50 <div class="form-submit">
51 <input type="submit" value="{% trans "Post" %}"/>
51 <input type="submit" value="{% trans "Post" %}"/>
52 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
52 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
53 </div>
53 </div>
54 </form>
54 </form>
55 </div>
55 </div>
56 <div id="preview-text"></div>
56 <div id="preview-text"></div>
57 <div><a href="{% url "staticpage" name="help" %}">
57 <div><a href="{% url "staticpage" name="help" %}">
58 {% trans 'Text syntax' %}</a></div>
58 {% trans 'Text syntax' %}</a></div>
59 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
59 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
60 </div>
60 </div>
61 </div>
61 </div>
62
62
63 <script src="{% static 'js/jquery.form.min.js' %}"></script>
63 <script src="{% static 'js/jquery.form.min.js' %}"></script>
64 {% endif %}
64 {% endif %}
65
65
66 <script src="{% static 'js/form.js' %}"></script>
66 <script src="{% static 'js/form.js' %}"></script>
67 <script src="{% static 'js/thread.js' %}"></script>
67 <script src="{% static 'js/thread.js' %}"></script>
68 <script src="{% static 'js/thread_update.js' %}"></script>
68 <script src="{% static 'js/thread_update.js' %}"></script>
69 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
69 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
70 {% endblock %}
70 {% endblock %}
@@ -1,49 +1,50 b''
1 import re
1 import re
2 from django.shortcuts import get_object_or_404
2 from django.shortcuts import get_object_or_404
3 from django import template
3 from django import template
4
4
5
5
6 IMG_ACTION_URL = '[<a href="{}">{}</a>]'
6 IMG_ACTION_URL = '[<a href="{}">{}</a>]'
7
7
8
8
9 register = template.Library()
9 register = template.Library()
10
10
11 actions = [
11 actions = [
12 {
12 {
13 'name': 'google',
13 'name': 'google',
14 'link': 'http://google.com/searchbyimage?image_url=%s',
14 'link': 'http://google.com/searchbyimage?image_url=%s',
15 },
15 },
16 {
16 {
17 'name': 'iqdb',
17 'name': 'iqdb',
18 'link': 'http://iqdb.org/?url=%s',
18 'link': 'http://iqdb.org/?url=%s',
19 },
19 },
20 ]
20 ]
21
21
22
22
23 @register.simple_tag(name='post_url')
23 @register.simple_tag(name='post_url')
24 def post_url(*args, **kwargs):
24 def post_url(*args, **kwargs):
25 post_id = args[0]
25 post_id = args[0]
26
26
27 post = get_object_or_404('Post', id=post_id)
27 post = get_object_or_404('Post', id=post_id)
28
28
29 return post.get_absolute_url()
29 return post.get_absolute_url()
30
30
31
31
32 @register.simple_tag(name='image_actions')
32 @register.simple_tag(name='image_actions')
33 def image_actions(*args, **kwargs):
33 def image_actions(*args, **kwargs):
34 image_link = args[0]
34 image_link = args[0]
35 if len(args) > 1:
35 if len(args) > 1:
36 image_link = 'http://' + args[1] + image_link # TODO https?
36 image_link = 'http://' + args[1] + image_link # TODO https?
37
37
38 return ', '.join([IMG_ACTION_URL.format(
38 return ', '.join([IMG_ACTION_URL.format(
39 action['link'] % image_link, action['name']) for action in actions])
39 action['link'] % image_link, action['name']) for action in actions])
40
40
41
41
42 @register.simple_tag(name='post_view')
42 @register.simple_tag(name='post_view', takes_context=True)
43 def post_view(post, *args, **kwargs):
43 def post_view(context, post, *args, **kwargs):
44 kwargs['perms'] = context['perms']
44 return post.get_view(*args, **kwargs)
45 return post.get_view(*args, **kwargs)
45
46
46 @register.simple_tag(name='page_url')
47 @register.simple_tag(name='page_url')
47 def page_url(paginator, page_number, *args, **kwargs):
48 def page_url(paginator, page_number, *args, **kwargs):
48 if paginator.supports_urls():
49 if paginator.supports_urls():
49 return paginator.get_page_url(page_number)
50 return paginator.get_page_url(page_number)
General Comments 0
You need to be logged in to leave comments. Login now