##// END OF EJS Templates
Reflinks to OPs are bold now. Refactored reflinks to build using the same code. Refactored autoescaping
neko259 -
r1309:a2eaff61 default
parent child Browse files
Show More
@@ -0,0 +1,21 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations
5 from boards.models import Post
6
7
8 class Migration(migrations.Migration):
9
10 def refuild_refmap(apps, schema_editor):
11 for post in Post.objects.all():
12 post.build_refmap()
13 post.save(update_fields=['refmap'])
14
15 dependencies = [
16 ('boards', '0024_post_tripcode'),
17 ]
18
19 operations = [
20 migrations.RunPython(refuild_refmap),
21 ]
@@ -1,230 +1,230 b''
1 1 # coding=utf-8
2 2
3 3 import re
4 4 import bbcode
5 5
6 6 from urllib.parse import unquote
7 7
8 8 from django.core.exceptions import ObjectDoesNotExist
9 9 from django.core.urlresolvers import reverse
10 10
11 11 import boards
12 12
13 13
14 14 __author__ = 'neko259'
15 15
16 16
17 17 REFLINK_PATTERN = re.compile(r'^\d+$')
18 18 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
19 19 ONE_NEWLINE = '\n'
20 20 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
21 21 LINE_BREAK_HTML = '<div class="br"></div>'
22 22
23 23
24 24 class TextFormatter():
25 25 """
26 26 An interface for formatter that can be used in the text format panel
27 27 """
28 28
29 29 def __init__(self):
30 30 pass
31 31
32 32 name = ''
33 33
34 34 # Left and right tags for the button preview
35 35 preview_left = ''
36 36 preview_right = ''
37 37
38 38 # Left and right characters for the textarea input
39 39 format_left = ''
40 40 format_right = ''
41 41
42 42
43 43 class AutolinkPattern():
44 44 def handleMatch(self, m):
45 45 link_element = etree.Element('a')
46 46 href = m.group(2)
47 47 link_element.set('href', href)
48 48 link_element.text = href
49 49
50 50 return link_element
51 51
52 52
53 53 class QuotePattern(TextFormatter):
54 54 name = '>q'
55 55 preview_left = '<span class="quote">'
56 56 preview_right = '</span>'
57 57
58 58 format_left = '[quote]'
59 59 format_right = '[/quote]'
60 60
61 61
62 62 class SpoilerPattern(TextFormatter):
63 63 name = 'spoiler'
64 64 preview_left = '<span class="spoiler">'
65 65 preview_right = '</span>'
66 66
67 67 format_left = '[spoiler]'
68 68 format_right = '[/spoiler]'
69 69
70 70 def handleMatch(self, m):
71 71 quote_element = etree.Element('span')
72 72 quote_element.set('class', 'spoiler')
73 73 quote_element.text = m.group(2)
74 74
75 75 return quote_element
76 76
77 77
78 78 class CommentPattern(TextFormatter):
79 79 name = ''
80 80 preview_left = '<span class="comment">// '
81 81 preview_right = '</span>'
82 82
83 83 format_left = '[comment]'
84 84 format_right = '[/comment]'
85 85
86 86
87 87 # TODO Use <s> tag here
88 88 class StrikeThroughPattern(TextFormatter):
89 89 name = 's'
90 90 preview_left = '<span class="strikethrough">'
91 91 preview_right = '</span>'
92 92
93 93 format_left = '[s]'
94 94 format_right = '[/s]'
95 95
96 96
97 97 class ItalicPattern(TextFormatter):
98 98 name = 'i'
99 99 preview_left = '<i>'
100 100 preview_right = '</i>'
101 101
102 102 format_left = '[i]'
103 103 format_right = '[/i]'
104 104
105 105
106 106 class BoldPattern(TextFormatter):
107 107 name = 'b'
108 108 preview_left = '<b>'
109 109 preview_right = '</b>'
110 110
111 111 format_left = '[b]'
112 112 format_right = '[/b]'
113 113
114 114
115 115 class CodePattern(TextFormatter):
116 116 name = 'code'
117 117 preview_left = '<code>'
118 118 preview_right = '</code>'
119 119
120 120 format_left = '[code]'
121 121 format_right = '[/code]'
122 122
123 123
124 124 def render_reflink(tag_name, value, options, parent, context):
125 125 result = '>>%s' % value
126 126
127 127 if REFLINK_PATTERN.match(value):
128 128 post_id = int(value)
129 129
130 130 try:
131 131 post = boards.models.Post.objects.get(id=post_id)
132 132
133 result = '<a href="%s">&gt;&gt;%s</a>' % (post.get_absolute_url(), post_id)
133 result = post.get_link_view()
134 134 except ObjectDoesNotExist:
135 135 pass
136 136
137 137 return result
138 138
139 139
140 140 def render_quote(tag_name, value, options, parent, context):
141 141 source = ''
142 142 if 'source' in options:
143 143 source = options['source']
144 144
145 145 if source:
146 146 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
147 147 else:
148 148 # Insert a ">" at the start of every line
149 149 result = '<span class="quote">&gt;{}</span>'.format(
150 150 value.replace(LINE_BREAK_HTML,
151 151 '{}&gt;'.format(LINE_BREAK_HTML)))
152 152
153 153 return result
154 154
155 155
156 156 def render_notification(tag_name, value, options, parent, content):
157 157 username = value.lower()
158 158
159 159 return '<a href="{}" class="user-cast">@{}</a>'.format(
160 160 reverse('notifications', kwargs={'username': username}), username)
161 161
162 162
163 163 def render_tag(tag_name, value, options, parent, context):
164 164 tag_name = value.lower()
165 165
166 166 try:
167 167 url = boards.models.Tag.objects.get(name=tag_name).get_view()
168 168 except ObjectDoesNotExist:
169 169 url = tag_name
170 170
171 171 return url
172 172
173 173
174 174 formatters = [
175 175 QuotePattern,
176 176 SpoilerPattern,
177 177 ItalicPattern,
178 178 BoldPattern,
179 179 CommentPattern,
180 180 StrikeThroughPattern,
181 181 CodePattern,
182 182 ]
183 183
184 184
185 185 PREPARSE_PATTERNS = {
186 186 r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
187 187 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
188 188 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
189 189 r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user"
190 190 }
191 191
192 192
193 193 class Parser:
194 194 def __init__(self):
195 195 # The newline hack is added because br's margin does not work in all
196 196 # browsers except firefox, when the div's does.
197 197 self.parser = bbcode.Parser(newline=LINE_BREAK_HTML)
198 198
199 199 self.parser.add_formatter('post', render_reflink, strip=True)
200 200 self.parser.add_formatter('quote', render_quote, strip=True)
201 201 self.parser.add_formatter('user', render_notification, strip=True)
202 202 self.parser.add_formatter('tag', render_tag, strip=True)
203 203 self.parser.add_simple_formatter(
204 204 'comment', '<span class="comment">//%(value)s</span>')
205 205 self.parser.add_simple_formatter(
206 206 'spoiler', '<span class="spoiler">%(value)s</span>')
207 207 self.parser.add_simple_formatter(
208 208 's', '<span class="strikethrough">%(value)s</span>')
209 209 # TODO Why not use built-in tag?
210 210 self.parser.add_simple_formatter('code',
211 211 '<pre><code>%(value)s</pre></code>',
212 212 render_embedded=False)
213 213
214 214 def preparse(self, text):
215 215 """
216 216 Performs manual parsing before the bbcode parser is used.
217 217 Preparsed text is saved as raw and the text before preparsing is lost.
218 218 """
219 219 new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
220 220
221 221 for key, value in PREPARSE_PATTERNS.items():
222 222 new_text = re.sub(key, value, new_text, flags=re.MULTILINE)
223 223
224 224 for link in REGEX_URL.findall(text):
225 225 new_text = new_text.replace(link, unquote(link))
226 226
227 227 return new_text
228 228
229 229 def parse(self, text):
230 230 return self.parser.format(text)
@@ -1,446 +1,456 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5 import uuid
6 6
7 7 from django.core.exceptions import ObjectDoesNotExist
8 8 from django.core.urlresolvers import reverse
9 9 from django.db import models, transaction
10 10 from django.db.models import TextField, QuerySet
11 11 from django.template.loader import render_to_string
12 12 from django.utils import timezone
13 13
14 14 from boards import settings
15 15 from boards.abstracts.tripcode import Tripcode
16 16 from boards.mdx_neboard import Parser
17 17 from boards.models import PostImage, Attachment
18 18 from boards.models.base import Viewable
19 19 from boards import utils
20 20 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
21 21 from boards.models.user import Notification, Ban
22 22 import boards.models.thread
23 23
24 24
25 25 APP_LABEL_BOARDS = 'boards'
26 26
27 27 POSTS_PER_DAY_RANGE = 7
28 28
29 29 BAN_REASON_AUTO = 'Auto'
30 30
31 31 IMAGE_THUMB_SIZE = (200, 150)
32 32
33 33 TITLE_MAX_LENGTH = 200
34 34
35 35 # TODO This should be removed
36 36 NO_IP = '0.0.0.0'
37 37
38 38 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
39 39 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
40 40
41 41 PARAMETER_TRUNCATED = 'truncated'
42 42 PARAMETER_TAG = 'tag'
43 43 PARAMETER_OFFSET = 'offset'
44 44 PARAMETER_DIFF_TYPE = 'type'
45 45 PARAMETER_CSS_CLASS = 'css_class'
46 46 PARAMETER_THREAD = 'thread'
47 47 PARAMETER_IS_OPENING = 'is_opening'
48 48 PARAMETER_MODERATOR = 'moderator'
49 49 PARAMETER_POST = 'post'
50 50 PARAMETER_OP_ID = 'opening_post_id'
51 51 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
52 52 PARAMETER_REPLY_LINK = 'reply_link'
53 53 PARAMETER_NEED_OP_DATA = 'need_op_data'
54 54
55 55 POST_VIEW_PARAMS = (
56 56 'need_op_data',
57 57 'reply_link',
58 58 'moderator',
59 59 'need_open_link',
60 60 'truncated',
61 61 'mode_tree',
62 62 )
63 63
64 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
65
66 64 IMAGE_TYPES = (
67 65 'jpeg',
68 66 'jpg',
69 67 'png',
70 68 'bmp',
71 69 'gif',
72 70 )
73 71
74 72
75 73 class PostManager(models.Manager):
76 74 @transaction.atomic
77 75 def create_post(self, title: str, text: str, file=None, thread=None,
78 76 ip=NO_IP, tags: list=None, opening_posts: list=None, tripcode=None):
79 77 """
80 78 Creates new post
81 79 """
82 80
83 81 is_banned = Ban.objects.filter(ip=ip).exists()
84 82
85 83 # TODO Raise specific exception and catch it in the views
86 84 if is_banned:
87 85 raise Exception("This user is banned")
88 86
89 87 if not tags:
90 88 tags = []
91 89 if not opening_posts:
92 90 opening_posts = []
93 91
94 92 posting_time = timezone.now()
95 93 new_thread = False
96 94 if not thread:
97 95 thread = boards.models.thread.Thread.objects.create(
98 96 bump_time=posting_time, last_edit_time=posting_time)
99 97 list(map(thread.tags.add, tags))
100 98 boards.models.thread.Thread.objects.process_oldest_threads()
101 99 new_thread = True
102 100
103 101 pre_text = Parser().preparse(text)
104 102
105 103 post = self.create(title=title,
106 104 text=pre_text,
107 105 pub_time=posting_time,
108 106 poster_ip=ip,
109 107 thread=thread,
110 108 last_edit_time=posting_time,
111 109 tripcode=tripcode)
112 110 post.threads.add(thread)
113 111
114 112 logger = logging.getLogger('boards.post.create')
115 113
116 114 logger.info('Created post {} by {}'.format(post, post.poster_ip))
117 115
118 116 # TODO Move this to other place
119 117 if file:
120 118 file_type = file.name.split('.')[-1].lower()
121 119 if file_type in IMAGE_TYPES:
122 120 post.images.add(PostImage.objects.create_with_hash(file))
123 121 else:
124 122 post.attachments.add(Attachment.objects.create_with_hash(file))
125 123
126 124 post.build_url()
127 125 post.connect_replies()
128 126 post.connect_threads(opening_posts)
129 127 post.connect_notifications()
130 128
131 129 # Thread needs to be bumped only when the post is already created
132 130 if not new_thread:
133 131 thread.last_edit_time = posting_time
134 132 thread.bump()
135 133 thread.save()
136 134
137 135 return post
138 136
139 137 def delete_posts_by_ip(self, ip):
140 138 """
141 139 Deletes all posts of the author with same IP
142 140 """
143 141
144 142 posts = self.filter(poster_ip=ip)
145 143 for post in posts:
146 144 post.delete()
147 145
148 146 @utils.cached_result()
149 147 def get_posts_per_day(self) -> float:
150 148 """
151 149 Gets average count of posts per day for the last 7 days
152 150 """
153 151
154 152 day_end = date.today()
155 153 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
156 154
157 155 day_time_start = timezone.make_aware(datetime.combine(
158 156 day_start, dtime()), timezone.get_current_timezone())
159 157 day_time_end = timezone.make_aware(datetime.combine(
160 158 day_end, dtime()), timezone.get_current_timezone())
161 159
162 160 posts_per_period = float(self.filter(
163 161 pub_time__lte=day_time_end,
164 162 pub_time__gte=day_time_start).count())
165 163
166 164 ppd = posts_per_period / POSTS_PER_DAY_RANGE
167 165
168 166 return ppd
169 167
170 168
171 169 class Post(models.Model, Viewable):
172 170 """A post is a message."""
173 171
174 172 objects = PostManager()
175 173
176 174 class Meta:
177 175 app_label = APP_LABEL_BOARDS
178 176 ordering = ('id',)
179 177
180 178 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
181 179 pub_time = models.DateTimeField()
182 180 text = TextField(blank=True, null=True)
183 181 _text_rendered = TextField(blank=True, null=True, editable=False)
184 182
185 183 images = models.ManyToManyField(PostImage, null=True, blank=True,
186 184 related_name='post_images', db_index=True)
187 185 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
188 186 related_name='attachment_posts')
189 187
190 188 poster_ip = models.GenericIPAddressField()
191 189
192 190 # TODO This field can be removed cause UID is used for update now
193 191 last_edit_time = models.DateTimeField()
194 192
195 193 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
196 194 null=True,
197 195 blank=True, related_name='refposts',
198 196 db_index=True)
199 197 refmap = models.TextField(null=True, blank=True)
200 198 threads = models.ManyToManyField('Thread', db_index=True)
201 199 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
202 200
203 201 url = models.TextField()
204 202 uid = models.TextField(db_index=True)
205 203
206 204 tripcode = models.CharField(max_length=50, null=True)
207 205
208 206 def __str__(self):
209 207 return 'P#{}/{}'.format(self.id, self.title)
210 208
211 209 def get_referenced_posts(self):
212 210 threads = self.get_threads().all()
213 211 return self.referenced_posts.filter(threads__in=threads)\
214 212 .order_by('pub_time').distinct().all()
215 213
216 214 def get_title(self) -> str:
217 215 """
218 216 Gets original post title or part of its text.
219 217 """
220 218
221 219 title = self.title
222 220 if not title:
223 221 title = self.get_text()
224 222
225 223 return title
226 224
227 225 def build_refmap(self) -> None:
228 226 """
229 227 Builds a replies map string from replies list. This is a cache to stop
230 228 the server from recalculating the map on every post show.
231 229 """
232 230
233 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
231 post_urls = [refpost.get_link_view()
234 232 for refpost in self.referenced_posts.all()]
235 233
236 234 self.refmap = ', '.join(post_urls)
237 235
238 236 def is_referenced(self) -> bool:
239 237 return self.refmap and len(self.refmap) > 0
240 238
241 239 def is_opening(self) -> bool:
242 240 """
243 241 Checks if this is an opening post or just a reply.
244 242 """
245 243
246 244 return self.get_thread().get_opening_post_id() == self.id
247 245
248 246 def get_absolute_url(self):
249 247 if self.url:
250 248 return self.url
251 249 else:
252 250 opening_id = self.get_thread().get_opening_post_id()
253 251 post_url = reverse('thread', kwargs={'post_id': opening_id})
254 252 if self.id != opening_id:
255 253 post_url += '#' + str(self.id)
256 254 return post_url
257 255
258 256
259 257 def get_thread(self):
260 258 return self.thread
261 259
262 260 def get_threads(self) -> QuerySet:
263 261 """
264 262 Gets post's thread.
265 263 """
266 264
267 265 return self.threads
268 266
269 267 def get_view(self, *args, **kwargs) -> str:
270 268 """
271 269 Renders post's HTML view. Some of the post params can be passed over
272 270 kwargs for the means of caching (if we view the thread, some params
273 271 are same for every post and don't need to be computed over and over.
274 272 """
275 273
276 274 thread = self.get_thread()
277 275 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
278 276
279 277 if is_opening:
280 278 opening_post_id = self.id
281 279 else:
282 280 opening_post_id = thread.get_opening_post_id()
283 281
284 282 css_class = 'post'
285 283 if thread.archived:
286 284 css_class += ' archive_post'
287 285 elif not thread.can_bump():
288 286 css_class += ' dead_post'
289 287
290 288 params = dict()
291 289 for param in POST_VIEW_PARAMS:
292 290 if param in kwargs:
293 291 params[param] = kwargs[param]
294 292
295 293 params.update({
296 294 PARAMETER_POST: self,
297 295 PARAMETER_IS_OPENING: is_opening,
298 296 PARAMETER_THREAD: thread,
299 297 PARAMETER_CSS_CLASS: css_class,
300 298 PARAMETER_OP_ID: opening_post_id,
301 299 })
302 300
303 301 return render_to_string('boards/post.html', params)
304 302
305 303 def get_search_view(self, *args, **kwargs):
306 304 return self.get_view(need_op_data=True, *args, **kwargs)
307 305
308 306 def get_first_image(self) -> PostImage:
309 307 return self.images.earliest('id')
310 308
311 309 def delete(self, using=None):
312 310 """
313 311 Deletes all post images and the post itself.
314 312 """
315 313
316 314 for image in self.images.all():
317 315 image_refs_count = image.post_images.count()
318 316 if image_refs_count == 1:
319 317 image.delete()
320 318
321 319 for attachment in self.attachments.all():
322 320 attachment_refs_count = attachment.attachment_posts.count()
323 321 if attachment_refs_count == 1:
324 322 attachment.delete()
325 323
326 324 thread = self.get_thread()
327 325 thread.last_edit_time = timezone.now()
328 326 thread.save()
329 327
330 328 super(Post, self).delete(using)
331 329
332 330 logging.getLogger('boards.post.delete').info(
333 331 'Deleted post {}'.format(self))
334 332
335 333 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
336 334 include_last_update=False) -> str:
337 335 """
338 336 Gets post HTML or JSON data that can be rendered on a page or used by
339 337 API.
340 338 """
341 339
342 340 return get_exporter(format_type).export(self, request,
343 341 include_last_update)
344 342
345 343 def notify_clients(self, recursive=True):
346 344 """
347 345 Sends post HTML data to the thread web socket.
348 346 """
349 347
350 348 if not settings.get_bool('External', 'WebsocketsEnabled'):
351 349 return
352 350
353 351 thread_ids = list()
354 352 for thread in self.get_threads().all():
355 353 thread_ids.append(thread.id)
356 354
357 355 thread.notify_clients()
358 356
359 357 if recursive:
360 358 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
361 359 post_id = reply_number.group(1)
362 360
363 361 try:
364 362 ref_post = Post.objects.get(id=post_id)
365 363
366 364 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
367 365 # If post is in this thread, its thread was already notified.
368 366 # Otherwise, notify its thread separately.
369 367 ref_post.notify_clients(recursive=False)
370 368 except ObjectDoesNotExist:
371 369 pass
372 370
373 371 def build_url(self):
374 372 self.url = self.get_absolute_url()
375 373 self.save(update_fields=['url'])
376 374
377 375 def save(self, force_insert=False, force_update=False, using=None,
378 376 update_fields=None):
379 377 self._text_rendered = Parser().parse(self.get_raw_text())
380 378
381 379 self.uid = str(uuid.uuid4())
382 380 if update_fields is not None and 'uid' not in update_fields:
383 381 update_fields += ['uid']
384 382
385 383 if self.id:
386 384 for thread in self.get_threads().all():
387 385 thread.last_edit_time = self.last_edit_time
388 386
389 387 thread.save(update_fields=['last_edit_time', 'bumpable'])
390 388
391 389 super().save(force_insert, force_update, using, update_fields)
392 390
393 391 def get_text(self) -> str:
394 392 return self._text_rendered
395 393
396 394 def get_raw_text(self) -> str:
397 395 return self.text
398 396
399 397 def get_absolute_id(self) -> str:
400 398 """
401 399 If the post has many threads, shows its main thread OP id in the post
402 400 ID.
403 401 """
404 402
405 403 if self.get_threads().count() > 1:
406 404 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
407 405 else:
408 406 return str(self.id)
409 407
410 408 def connect_notifications(self):
411 409 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
412 410 user_name = reply_number.group(1).lower()
413 411 Notification.objects.get_or_create(name=user_name, post=self)
414 412
415 413 def connect_replies(self):
416 414 """
417 415 Connects replies to a post to show them as a reflink map
418 416 """
419 417
420 418 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
421 419 post_id = reply_number.group(1)
422 420
423 421 try:
424 422 referenced_post = Post.objects.get(id=post_id)
425 423
426 424 referenced_post.referenced_posts.add(self)
427 425 referenced_post.last_edit_time = self.pub_time
428 426 referenced_post.build_refmap()
429 427 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
430 428 except ObjectDoesNotExist:
431 429 pass
432 430
433 431 def connect_threads(self, opening_posts):
434 432 for opening_post in opening_posts:
435 433 threads = opening_post.get_threads().all()
436 434 for thread in threads:
437 435 if thread.can_bump():
438 436 thread.update_bump_status()
439 437
440 438 thread.last_edit_time = self.last_edit_time
441 439 thread.save(update_fields=['last_edit_time', 'bumpable'])
442 440 self.threads.add(opening_post.get_thread())
443 441
444 442 def get_tripcode(self):
445 443 if self.tripcode:
446 444 return Tripcode(self.tripcode)
445
446 def get_link_view(self):
447 """
448 Gets view of a reflink to the post.
449 """
450
451 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
452 self.id)
453 if self.is_opening():
454 result = '<b>{}</b>'.format(result)
455
456 return result
@@ -1,192 +1,186 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load board %}
5 5 {% load static %}
6 6 {% load tz %}
7 7
8 8 {% block head %}
9 9 <meta name="robots" content="noindex">
10 10
11 11 {% if tag %}
12 12 <title>{{ tag.name }} - {{ site_name }}</title>
13 13 {% else %}
14 14 <title>{{ site_name }}</title>
15 15 {% endif %}
16 16
17 17 {% if prev_page_link %}
18 18 <link rel="prev" href="{{ prev_page_link }}" />
19 19 {% endif %}
20 20 {% if next_page_link %}
21 21 <link rel="next" href="{{ next_page_link }}" />
22 22 {% endif %}
23 23
24 24 {% endblock %}
25 25
26 26 {% block content %}
27 27
28 28 {% get_current_language as LANGUAGE_CODE %}
29 29 {% get_current_timezone as TIME_ZONE %}
30 30
31 31 {% for banner in banners %}
32 32 <div class="post">
33 33 <div class="title">{{ banner.title }}</div>
34 34 <div>{{ banner.text }}</div>
35 35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 36 </div>
37 37 {% endfor %}
38 38
39 39 {% if tag %}
40 40 <div class="tag_info">
41 41 {% if random_image_post %}
42 42 <div class="tag-image">
43 43 {% with image=random_image_post.images.first %}
44 44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 45 src="{{ image.image.url_200x150 }}"
46 46 width="{{ image.pre_width }}"
47 47 height="{{ image.pre_height }}"/></a>
48 48 {% endwith %}
49 49 </div>
50 50 {% endif %}
51 51 <div class="tag-text-data">
52 52 <h2>
53 53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
54 54 {% if is_favorite %}
55 55 <button name="method" value="unsubscribe" class="fav"></button>
56 56 {% else %}
57 57 <button name="method" value="subscribe" class="not_fav"></button>
58 58 {% endif %}
59 59 </form>
60 60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
61 61 {% if is_hidden %}
62 62 <button name="method" value="unhide" class="fav">H</button>
63 63 {% else %}
64 64 <button name="method" value="hide" class="not_fav">H</button>
65 65 {% endif %}
66 66 </form>
67 {% autoescape off %}
68 {{ tag.get_view }}
69 {% endautoescape %}
67 {{ tag.get_view|safe }}
70 68 {% if moderator %}
71 69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
72 70 {% endif %}
73 71 </h2>
74 72 {% if tag.get_description %}
75 {% autoescape off %}
76 <p>{{ tag.get_description }}</p>
77 {% endautoescape %}
73 <p>{{ tag.get_description|safe }}</p>
78 74 {% endif %}
79 75 <p>{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}</p>
80 76 {% if related_tags %}
81 77 <p>{% trans 'Related tags:' %}
82 78 {% for rel_tag in related_tags %}
83 {% autoescape off %}
84 {{ rel_tag.get_view }}{% if not forloop.last %}, {% else %}.{% endif %}
85 {% endautoescape %}
79 {{ rel_tag.get_view|safe }}{% if not forloop.last %}, {% else %}.{% endif %}
86 80 {% endfor %}
87 81 </p>
88 82 {% endif %}
89 83 </div>
90 84 </div>
91 85 {% endif %}
92 86
93 87 {% if threads %}
94 88 {% if prev_page_link %}
95 89 <div class="page_link">
96 90 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
97 91 </div>
98 92 {% endif %}
99 93
100 94 {% for thread in threads %}
101 95 <div class="thread">
102 96 {% post_view thread.get_opening_post moderator=moderator is_opening=True thread=thread truncated=True need_open_link=True %}
103 97 {% if not thread.archived %}
104 98 {% with last_replies=thread.get_last_replies %}
105 99 {% if last_replies %}
106 100 {% with skipped_replies_count=thread.get_skipped_replies_count %}
107 101 {% if skipped_replies_count %}
108 102 <div class="skipped_replies">
109 103 <a href="{% url 'thread' thread.get_opening_post_id %}">
110 104 {% blocktrans with count=skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
111 105 </a>
112 106 </div>
113 107 {% endif %}
114 108 {% endwith %}
115 109 <div class="last-replies">
116 110 {% for post in last_replies %}
117 111 {% post_view post is_opening=False moderator=moderator truncated=True %}
118 112 {% endfor %}
119 113 </div>
120 114 {% endif %}
121 115 {% endwith %}
122 116 {% endif %}
123 117 </div>
124 118 {% endfor %}
125 119
126 120 {% if next_page_link %}
127 121 <div class="page_link">
128 122 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
129 123 </div>
130 124 {% endif %}
131 125 {% else %}
132 126 <div class="post">
133 127 {% trans 'No threads exist. Create the first one!' %}</div>
134 128 {% endif %}
135 129
136 130 <div class="post-form-w">
137 131 <script src="{% static 'js/panel.js' %}"></script>
138 132 <div class="post-form">
139 133 <div class="form-title">{% trans "Create new thread" %}</div>
140 134 <div class="swappable-form-full">
141 135 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
142 136 {{ form.as_div }}
143 137 <div class="form-submit">
144 138 <input type="submit" value="{% trans "Post" %}"/>
145 139 </div>
146 140 </form>
147 141 </div>
148 142 <div>
149 143 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
150 144 </div>
151 145 <div><button id="preview-button">{% trans 'Preview' %}</button></div>
152 146 <div id="preview-text"></div>
153 147 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
154 148 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
155 149 </div>
156 150 </div>
157 151
158 152 <script src="{% static 'js/form.js' %}"></script>
159 153 <script src="{% static 'js/thread_create.js' %}"></script>
160 154
161 155 {% endblock %}
162 156
163 157 {% block metapanel %}
164 158
165 159 <span class="metapanel">
166 160 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
167 161 {% trans "Pages:" %}
168 162 [
169 163 {% with dividers=paginator.get_dividers %}
170 164 {% for page in paginator.get_divided_range %}
171 165 {% if page in dividers %}
172 166 …,
173 167 {% endif %}
174 168 <a
175 169 {% ifequal page current_page.number %}
176 170 class="current_page"
177 171 {% endifequal %}
178 172 href="
179 173 {% if tag %}
180 174 {% url "tag" tag_name=tag.name %}?page={{ page }}
181 175 {% else %}
182 176 {% url "index" %}?page={{ page }}
183 177 {% endif %}
184 178 ">{{ page }}</a>
185 179 {% if not forloop.last %},{% endif %}
186 180 {% endfor %}
187 181 {% endwith %}
188 182 ]
189 183 [<a href="rss/">RSS</a>]
190 184 </span>
191 185
192 186 {% endblock %}
@@ -1,117 +1,109 b''
1 1 {% load i18n %}
2 2 {% load board %}
3 3
4 4 {% get_current_language as LANGUAGE_CODE %}
5 5
6 6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
7 7 <div class="post-info">
8 8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
9 9 <span class="title">{{ post.title }}</span>
10 10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
11 11 {% if post.tripcode %}
12 12 {% with tripcode=post.get_tripcode %}
13 13 <a href="{% url 'feed' %}?tripcode={{ tripcode.get_full_text }}"
14 14 class="tripcode" title="{{ tripcode.get_full_text }}"
15 15 style="border: solid 2px #{{ tripcode.get_color }}; border-left: solid 1ex #{{ tripcode.get_color }};">{{ tripcode.get_short_text }}</a>
16 16 {% endwith %}
17 17 {% endif %}
18 18 {% comment %}
19 19 Thread death time needs to be shown only if the thread is alredy archived
20 20 and this is an opening post (thread death time) or a post for popup
21 21 (we don't see OP here so we show the death time in the post itself).
22 22 {% endcomment %}
23 23 {% if thread.archived %}
24 24 {% if is_opening %}
25 25 <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
26 26 {% endif %}
27 27 {% endif %}
28 28 {% if is_opening %}
29 29 {% if need_open_link %}
30 30 {% if thread.archived %}
31 31 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
32 32 {% else %}
33 33 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
34 34 {% endif %}
35 35 {% endif %}
36 36 {% else %}
37 37 {% if need_op_data %}
38 38 {% with thread.get_opening_post as op %}
39 {% trans " in " %}<a href="{{ op.get_absolute_url }}">&gt;&gt;{{ op.id }}</a> <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
39 {% trans " in " %}{{ op.get_link_view|safe }} <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
40 40 {% endwith %}
41 41 {% endif %}
42 42 {% endif %}
43 43 {% if reply_link and not thread.archived %}
44 44 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
45 45 {% endif %}
46 46
47 47 {% if moderator %}
48 48 <span class="moderator_info">
49 49 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
50 50 {% if is_opening %}
51 51 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
52 52 {% endif %}
53 53 </span>
54 54 {% endif %}
55 55 </div>
56 56 {% comment %}
57 57 Post images. Currently only 1 image can be posted and shown, but post model
58 58 supports multiple.
59 59 {% endcomment %}
60 60 {% if post.images.exists %}
61 61 {% with post.images.first as image %}
62 {% autoescape off %}
63 {{ image.get_view }}
64 {% endautoescape %}
62 {{ image.get_view|safe }}
65 63 {% endwith %}
66 64 {% endif %}
67 65 {% if post.attachments.exists %}
68 66 {% with post.attachments.first as file %}
69 {% autoescape off %}
70 {{ file.get_view }}
71 {% endautoescape %}
67 {{ file.get_view|safe }}
72 68 {% endwith %}
73 69 {% endif %}
74 70 {% comment %}
75 71 Post message (text)
76 72 {% endcomment %}
77 73 <div class="message">
78 74 {% autoescape off %}
79 75 {% if truncated %}
80 76 {{ post.get_text|truncatewords_html:50 }}
81 77 {% else %}
82 78 {{ post.get_text }}
83 79 {% endif %}
84 80 {% endautoescape %}
85 81 </div>
86 82 {% if post.is_referenced %}
87 83 {% if mode_tree %}
88 84 <div class="tree_reply">
89 85 {% for refpost in post.get_referenced_posts %}
90 86 {% post_view refpost mode_tree=True %}
91 87 {% endfor %}
92 88 </div>
93 89 {% else %}
94 90 <div class="refmap">
95 {% autoescape off %}
96 {% trans "Replies" %}: {{ post.refmap }}
97 {% endautoescape %}
91 {% trans "Replies" %}: {{ post.refmap|safe }}
98 92 </div>
99 93 {% endif %}
100 94 {% endif %}
101 95 {% comment %}
102 96 Thread metadata: counters, tags etc
103 97 {% endcomment %}
104 98 {% if is_opening %}
105 99 <div class="metadata">
106 100 {% if is_opening and need_open_link %}
107 101 {{ thread.get_reply_count }} {% trans 'messages' %},
108 102 {{ thread.get_images_count }} {% trans 'images' %}.
109 103 {% endif %}
110 104 <span class="tags">
111 {% autoescape off %}
112 {{ thread.get_tag_url_list }}
113 {% endautoescape %}
105 {{ thread.get_tag_url_list|safe }}
114 106 </span>
115 107 </div>
116 108 {% endif %}
117 109 </div>
@@ -1,42 +1,40 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load tz %}
5 5
6 6 {% block head %}
7 7 <meta name="robots" content="noindex">
8 8 <title>{% trans 'Settings' %} - {{ site_name }}</title>
9 9 {% endblock %}
10 10
11 11 {% block content %}
12 12 <div class="post">
13 13 <p>
14 14 {% if moderator %}
15 15 {% trans 'You are moderator.' %}
16 16 {% endif %}
17 17 </p>
18 18 {% if hidden_tags %}
19 19 <p>{% trans 'Hidden tags:' %}
20 20 {% for tag in hidden_tags %}
21 {% autoescape off %}
22 {{ tag.get_view }}
23 {% endautoescape %}
21 {{ tag.get_view|safe }}
24 22 {% endfor %}
25 23 </p>
26 24 {% else %}
27 25 <p>{% trans 'No hidden tags.' %}</p>
28 26 {% endif %}
29 27 </div>
30 28
31 29 <div class="post-form-w">
32 30 <div class="post-form">
33 31 <form method="post">{% csrf_token %}
34 32 {{ form.as_div }}
35 33 <div class="form-submit">
36 34 <input type="submit" value="{% trans "Save" %}" />
37 35 </div>
38 36 </form>
39 37 </div>
40 38 </div>
41 39
42 40 {% endblock %}
@@ -1,5 +1,3 b''
1 1 <div class="post">
2 {% autoescape off %}
3 {{ tag.get_view }}
4 {% endautoescape %}
2 {{ tag.get_view|safe }}
5 3 </div>
General Comments 0
You need to be logged in to leave comments. Login now