##// END OF EJS Templates
Post model code refacoring. Added help on the "thread" parser tag
neko259 -
r1063:fbac3cd5 default
parent child Browse files
Show More
1 NO CONTENT: modified file, binary diff hidden
@@ -1,373 +1,377 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 msgid ""
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2015-03-26 20:40+0200\n"
10 "POT-Creation-Date: 2015-03-29 16:19+0300\n"
11 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 14 "Language: ru\n"
15 15 "MIME-Version: 1.0\n"
16 16 "Content-Type: text/plain; charset=UTF-8\n"
17 17 "Content-Transfer-Encoding: 8bit\n"
18 18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 20
21 21 #: admin.py:22
22 22 msgid "{} posters were banned"
23 23 msgstr ""
24 24
25 25 #: authors.py:9
26 26 msgid "author"
27 27 msgstr "автор"
28 28
29 29 #: authors.py:10
30 30 msgid "developer"
31 31 msgstr "разработчик"
32 32
33 33 #: authors.py:11
34 34 msgid "javascript developer"
35 35 msgstr "разработчик javascript"
36 36
37 37 #: authors.py:12
38 38 msgid "designer"
39 39 msgstr "дизайнер"
40 40
41 41 #: forms.py:33
42 42 msgid "Type message here. Use formatting panel for more advanced usage."
43 43 msgstr ""
44 44 "Вводите сообщение сюда. Используйте панель для более сложного форматирования."
45 45
46 46 #: forms.py:34
47 47 msgid "tag1 several_words_tag"
48 48 msgstr "метка1 метка_из_нескольких_слов"
49 49
50 50 #: forms.py:36
51 51 msgid "Title"
52 52 msgstr "Заголовок"
53 53
54 54 #: forms.py:37
55 55 msgid "Text"
56 56 msgstr "Текст"
57 57
58 58 #: forms.py:38
59 59 msgid "Tag"
60 60 msgstr "Метка"
61 61
62 62 #: forms.py:39 templates/boards/base.html:36 templates/search/search.html:13
63 63 #: templates/search/search.html.py:17
64 64 msgid "Search"
65 65 msgstr "Поиск"
66 66
67 67 #: forms.py:131
68 68 msgid "Image"
69 69 msgstr "Изображение"
70 70
71 71 #: forms.py:134
72 72 msgid "Image URL"
73 73 msgstr "URL изображения"
74 74
75 75 #: forms.py:140
76 76 msgid "e-mail"
77 77 msgstr ""
78 78
79 79 #: forms.py:151
80 80 #, python-format
81 81 msgid "Title must have less than %s characters"
82 82 msgstr "Заголовок должен иметь меньше %s символов"
83 83
84 84 #: forms.py:160
85 85 #, python-format
86 86 msgid "Text must have less than %s characters"
87 87 msgstr "Текст должен быть короче %s символов"
88 88
89 89 #: forms.py:182
90 90 msgid "Invalid URL"
91 91 msgstr "Неверный URL"
92 92
93 93 #: forms.py:219
94 94 msgid "Either text or image must be entered."
95 95 msgstr "Текст или картинка должны быть введены."
96 96
97 97 #: forms.py:235
98 98 #, python-format
99 99 msgid "Wait %s seconds after last posting"
100 100 msgstr "Подождите %s секунд после последнего постинга"
101 101
102 102 #: forms.py:247
103 103 #, python-format
104 104 msgid "Image must be less than %s bytes"
105 105 msgstr "Изображение должно быть менее %s байт"
106 106
107 107 #: forms.py:294 templates/boards/rss/post.html:10 templates/boards/tags.html:7
108 108 msgid "Tags"
109 109 msgstr "Метки"
110 110
111 111 #: forms.py:301
112 112 msgid "Inappropriate characters in tags."
113 113 msgstr "Недопустимые символы в метках."
114 114
115 115 #: forms.py:312
116 116 msgid "Need at least 1 required tag."
117 117 msgstr "Нужна хотя бы 1 обязательная метка."
118 118
119 119 #: forms.py:325
120 120 msgid "Theme"
121 121 msgstr "Тема"
122 122
123 123 #: forms.py:326
124 124 msgid "User name"
125 125 msgstr "Имя пользователя"
126 126
127 127 #: forms.py:332
128 128 msgid "Inappropriate characters."
129 129 msgstr "Недопустимые символы."
130 130
131 131 #: templates/boards/404.html:6
132 132 msgid "Not found"
133 133 msgstr "Не найдено"
134 134
135 135 #: templates/boards/404.html:12
136 136 msgid "This page does not exist"
137 137 msgstr "Этой страницы не существует"
138 138
139 139 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
140 140 msgid "Authors"
141 141 msgstr "Авторы"
142 142
143 143 #: templates/boards/authors.html:26
144 144 msgid "Distributed under the"
145 145 msgstr "Распространяется под"
146 146
147 147 #: templates/boards/authors.html:28
148 148 msgid "license"
149 149 msgstr "лицензией"
150 150
151 151 #: templates/boards/authors.html:30
152 152 msgid "Repository"
153 153 msgstr "Репозиторий"
154 154
155 155 #: templates/boards/base.html:13
156 156 msgid "Feed"
157 157 msgstr "Лента"
158 158
159 159 #: templates/boards/base.html:30
160 160 msgid "All threads"
161 161 msgstr "Все темы"
162 162
163 163 #: templates/boards/base.html:34
164 164 msgid "Tag management"
165 165 msgstr "Управление метками"
166 166
167 167 #: templates/boards/base.html:39 templates/boards/base.html.py:40
168 168 #: templates/boards/notifications.html:8
169 169 msgid "Notifications"
170 170 msgstr "Уведомления"
171 171
172 172 #: templates/boards/base.html:47 templates/boards/settings.html:8
173 173 msgid "Settings"
174 174 msgstr "Настройки"
175 175
176 176 #: templates/boards/base.html:60
177 177 msgid "Admin"
178 178 msgstr "Администрирование"
179 179
180 180 #: templates/boards/base.html:62
181 181 #, python-format
182 182 msgid "Speed: %(ppd)s posts per day"
183 183 msgstr "Скорость: %(ppd)s сообщений в день"
184 184
185 185 #: templates/boards/base.html:64
186 186 msgid "Up"
187 187 msgstr "Вверх"
188 188
189 189 #: templates/boards/notifications.html:17
190 190 #: templates/boards/posting_general.html:79 templates/search/search.html:26
191 191 msgid "Previous page"
192 192 msgstr "Предыдущая страница"
193 193
194 194 #: templates/boards/notifications.html:27
195 195 #: templates/boards/posting_general.html:119 templates/search/search.html:37
196 196 msgid "Next page"
197 197 msgstr "Следующая страница"
198 198
199 199 #: templates/boards/post.html:32
200 200 msgid "Open"
201 201 msgstr "Открыть"
202 202
203 203 #: templates/boards/post.html:34 templates/boards/post.html.py:38
204 204 msgid "Reply"
205 205 msgstr "Ответ"
206 206
207 207 #: templates/boards/post.html:43
208 208 msgid "Edit"
209 209 msgstr "Изменить"
210 210
211 211 #: templates/boards/post.html:45
212 212 msgid "Edit thread"
213 213 msgstr "Изменить тему"
214 214
215 215 #: templates/boards/post.html:75
216 216 msgid "Replies"
217 217 msgstr "Ответы"
218 218
219 219 #: templates/boards/post.html:86 templates/boards/thread.html:28
220 220 msgid "messages"
221 221 msgstr "сообщений"
222 222
223 223 #: templates/boards/post.html:87 templates/boards/thread.html:29
224 224 msgid "images"
225 225 msgstr "изображений"
226 226
227 227 #: templates/boards/posting_general.html:63
228 228 msgid "Edit tag"
229 229 msgstr "Изменить метку"
230 230
231 231 #: templates/boards/posting_general.html:66
232 232 #, python-format
233 233 msgid "This tag has %(thread_count)s threads and %(post_count)s posts."
234 234 msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений."
235 235
236 236 #: templates/boards/posting_general.html:94
237 237 #, python-format
238 238 msgid "Skipped %(count)s replies. Open thread to see all replies."
239 239 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
240 240
241 241 #: templates/boards/posting_general.html:124
242 242 msgid "No threads exist. Create the first one!"
243 243 msgstr "Нет тем. Создайте первую!"
244 244
245 245 #: templates/boards/posting_general.html:130
246 246 msgid "Create new thread"
247 247 msgstr "Создать новую тему"
248 248
249 249 #: templates/boards/posting_general.html:135 templates/boards/preview.html:16
250 250 #: templates/boards/thread_normal.html:44
251 251 msgid "Post"
252 252 msgstr "Отправить"
253 253
254 254 #: templates/boards/posting_general.html:141
255 255 msgid "Tags must be delimited by spaces. Text or image is required."
256 256 msgstr ""
257 257 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
258 258
259 259 #: templates/boards/posting_general.html:144
260 260 #: templates/boards/thread_normal.html:50
261 261 msgid "Text syntax"
262 262 msgstr "Синтаксис текста"
263 263
264 264 #: templates/boards/posting_general.html:156
265 265 msgid "Pages:"
266 266 msgstr "Страницы: "
267 267
268 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:19
268 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20
269 269 msgid "Preview"
270 270 msgstr "Предпросмотр"
271 271
272 272 #: templates/boards/rss/post.html:5
273 273 msgid "Post image"
274 274 msgstr "Изображение сообщения"
275 275
276 276 #: templates/boards/settings.html:16
277 277 msgid "You are moderator."
278 278 msgstr "Вы модератор."
279 279
280 280 #: templates/boards/settings.html:20
281 281 msgid "Hidden tags:"
282 282 msgstr "Скрытые метки:"
283 283
284 284 #: templates/boards/settings.html:28
285 285 msgid "No hidden tags."
286 286 msgstr "Нет скрытых меток."
287 287
288 288 #: templates/boards/settings.html:37
289 289 msgid "Save"
290 290 msgstr "Сохранить"
291 291
292 292 #: templates/boards/staticpages/banned.html:6
293 293 msgid "Banned"
294 294 msgstr "Заблокирован"
295 295
296 296 #: templates/boards/staticpages/banned.html:11
297 297 msgid "Your IP address has been banned. Contact the administrator"
298 298 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
299 299
300 300 #: templates/boards/staticpages/help.html:6
301 301 #: templates/boards/staticpages/help.html:10
302 302 msgid "Syntax"
303 303 msgstr "Синтаксис"
304 304
305 305 #: templates/boards/staticpages/help.html:11
306 306 msgid "Italic text"
307 307 msgstr "Курсивный текст"
308 308
309 309 #: templates/boards/staticpages/help.html:12
310 310 msgid "Bold text"
311 311 msgstr "Полужирный текст"
312 312
313 313 #: templates/boards/staticpages/help.html:13
314 314 msgid "Spoiler"
315 315 msgstr "Спойлер"
316 316
317 317 #: templates/boards/staticpages/help.html:14
318 318 msgid "Link to a post"
319 319 msgstr "Ссылка на сообщение"
320 320
321 321 #: templates/boards/staticpages/help.html:15
322 msgid "Add post to this thread"
323 msgstr "Добавить сообщение в эту тему"
324
325 #: templates/boards/staticpages/help.html:16
322 326 msgid "Strikethrough text"
323 327 msgstr "Зачеркнутый текст"
324 328
325 #: templates/boards/staticpages/help.html:16
329 #: templates/boards/staticpages/help.html:17
326 330 msgid "Comment"
327 331 msgstr "Комментарий"
328 332
329 #: templates/boards/staticpages/help.html:17
333 #: templates/boards/staticpages/help.html:18
330 334 msgid "Quote"
331 335 msgstr "Цитата"
332 336
333 #: templates/boards/staticpages/help.html:19
337 #: templates/boards/staticpages/help.html:20
334 338 msgid "You can try pasting the text and previewing the result here:"
335 339 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
336 340
337 341 #: templates/boards/tags.html:23
338 342 msgid "No tags found."
339 343 msgstr "Метки не найдены."
340 344
341 345 #: templates/boards/thread.html:30
342 346 msgid "Last update: "
343 347 msgstr "Последнее обновление: "
344 348
345 349 #: templates/boards/thread_gallery.html:20
346 350 #: templates/boards/thread_normal.html:14
347 351 msgid "Normal mode"
348 352 msgstr "Нормальный режим"
349 353
350 354 #: templates/boards/thread_gallery.html:21
351 355 #: templates/boards/thread_normal.html:15
352 356 msgid "Gallery mode"
353 357 msgstr "Режим галереи"
354 358
355 359 #: templates/boards/thread_gallery.html:51
356 360 msgid "No images."
357 361 msgstr "Нет изображений."
358 362
359 363 #: templates/boards/thread_normal.html:23
360 364 msgid "posts to bumplimit"
361 365 msgstr "сообщений до бамплимита"
362 366
363 367 #: templates/boards/thread_normal.html:37
364 368 msgid "Reply to thread"
365 369 msgstr "Ответить в тему"
366 370
367 371 #: templates/boards/thread_normal.html:51
368 372 msgid "Close form"
369 373 msgstr "Закрыть форму"
370 374
371 375 #: templates/boards/thread_normal.html:67
372 376 msgid "Update"
373 377 msgstr "Обновить"
@@ -1,489 +1,476 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
6 6 from urllib.parse import unquote
7 7
8 8 from adjacent import Client
9 9 from django.core.exceptions import ObjectDoesNotExist
10 10 from django.core.urlresolvers import reverse
11 11 from django.db import models, transaction
12 12 from django.db.models import TextField
13 13 from django.template.loader import render_to_string
14 14 from django.utils import timezone
15 15
16 16 from boards import settings
17 17 from boards.mdx_neboard import bbcode_extended
18 18 from boards.models import PostImage
19 19 from boards.models.base import Viewable
20 20 from boards.utils import datetime_to_epoch, cached_result
21 21 from boards.models.user import Notification
22 22 import boards.models.thread
23 23
24 24
25 25 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
26 26 WS_NOTIFICATION_TYPE = 'notification_type'
27 27
28 28 WS_CHANNEL_THREAD = "thread:"
29 29
30 30 APP_LABEL_BOARDS = 'boards'
31 31
32 32 POSTS_PER_DAY_RANGE = 7
33 33
34 34 BAN_REASON_AUTO = 'Auto'
35 35
36 36 IMAGE_THUMB_SIZE = (200, 150)
37 37
38 38 TITLE_MAX_LENGTH = 200
39 39
40 40 # TODO This should be removed
41 41 NO_IP = '0.0.0.0'
42 42
43 43 # TODO Real user agent should be saved instead of this
44 44 UNKNOWN_UA = ''
45 45
46 46 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
47 47 REGEX_MULTI_THREAD = re.compile(r'\[thread\](\d+)\[/thread\]')
48 48 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
49 49 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
50 50
51 51 PARAMETER_TRUNCATED = 'truncated'
52 52 PARAMETER_TAG = 'tag'
53 53 PARAMETER_OFFSET = 'offset'
54 54 PARAMETER_DIFF_TYPE = 'type'
55 55 PARAMETER_BUMPABLE = 'bumpable'
56 56 PARAMETER_THREAD = 'thread'
57 57 PARAMETER_IS_OPENING = 'is_opening'
58 58 PARAMETER_MODERATOR = 'moderator'
59 59 PARAMETER_POST = 'post'
60 60 PARAMETER_OP_ID = 'opening_post_id'
61 61 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
62 62 PARAMETER_REPLY_LINK = 'reply_link'
63 63
64 64 DIFF_TYPE_HTML = 'html'
65 65 DIFF_TYPE_JSON = 'json'
66 66
67 67 PREPARSE_PATTERNS = {
68 68 r'>>>(\d+)': r'[thread]\1[/thread]', # Multi-thread post ">>>123"
69 69 r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
70 70 r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
71 71 r'^//(.+)': r'[comment]\1[/comment]', # Comment "//text"
72 72 r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user"
73 73 }
74 74
75 75
76 76 class PostManager(models.Manager):
77 77 @transaction.atomic
78 78 def create_post(self, title: str, text: str, image=None, thread=None,
79 79 ip=NO_IP, tags: list=None):
80 80 """
81 81 Creates new post
82 82 """
83 83
84 84 if not tags:
85 85 tags = []
86 86
87 87 posting_time = timezone.now()
88 88 if not thread:
89 89 thread = boards.models.thread.Thread.objects.create(
90 bump_time=posting_time, last_edit_time=posting_time)
90 bump_time=posting_time, last_edit_time=posting_time)
91 91 new_thread = True
92 92 else:
93 93 new_thread = False
94 94
95 95 pre_text = self._preparse_text(text)
96 96
97 97 post = self.create(title=title,
98 98 text=pre_text,
99 99 pub_time=posting_time,
100 100 poster_ip=ip,
101 101 thread=thread,
102 102 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
103 103 # last!
104 104 last_edit_time=posting_time)
105 105 post.threads.add(thread)
106 106
107 107 logger = logging.getLogger('boards.post.create')
108 108
109 109 logger.info('Created post {} by {}'.format(
110 110 post, post.poster_ip))
111 111
112 112 if image:
113 113 post.images.add(PostImage.objects.create_with_hash(image))
114 114
115 115 list(map(thread.add_tag, tags))
116 116
117 117 if new_thread:
118 118 boards.models.thread.Thread.objects.process_oldest_threads()
119 119 else:
120 120 thread.last_edit_time = posting_time
121 121 thread.bump()
122 122 thread.save()
123 123
124 124 post.connect_replies()
125 125 post.connect_threads()
126 126 post.connect_notifications()
127 127
128 128 return post
129 129
130 130 def delete_posts_by_ip(self, ip):
131 131 """
132 132 Deletes all posts of the author with same IP
133 133 """
134 134
135 135 posts = self.filter(poster_ip=ip)
136 136 for post in posts:
137 137 post.delete()
138 138
139 139 @cached_result
140 140 def get_posts_per_day(self):
141 141 """
142 142 Gets average count of posts per day for the last 7 days
143 143 """
144 144
145 145 day_end = date.today()
146 146 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
147 147
148 148 day_time_start = timezone.make_aware(datetime.combine(
149 149 day_start, dtime()), timezone.get_current_timezone())
150 150 day_time_end = timezone.make_aware(datetime.combine(
151 151 day_end, dtime()), timezone.get_current_timezone())
152 152
153 153 posts_per_period = float(self.filter(
154 154 pub_time__lte=day_time_end,
155 155 pub_time__gte=day_time_start).count())
156 156
157 157 ppd = posts_per_period / POSTS_PER_DAY_RANGE
158 158
159 159 return ppd
160 160
161 161 # TODO Make a separate parser module and move preparser there
162 162 def _preparse_text(self, text: str) -> str:
163 163 """
164 164 Preparses text to change patterns like '>>' to a proper bbcode
165 165 tags.
166 166 """
167 167
168 168 for key, value in PREPARSE_PATTERNS.items():
169 169 text = re.sub(key, value, text, flags=re.MULTILINE)
170 170
171 171 for link in REGEX_URL.findall(text):
172 172 text = text.replace(link, unquote(link))
173 173
174 174 return text
175 175
176 176
177 177 class Post(models.Model, Viewable):
178 178 """A post is a message."""
179 179
180 180 objects = PostManager()
181 181
182 182 class Meta:
183 183 app_label = APP_LABEL_BOARDS
184 184 ordering = ('id',)
185 185
186 186 title = models.CharField(max_length=TITLE_MAX_LENGTH)
187 187 pub_time = models.DateTimeField()
188 188 text = TextField(blank=True, null=True)
189 189 _text_rendered = TextField(blank=True, null=True, editable=False)
190 190
191 191 images = models.ManyToManyField(PostImage, null=True, blank=True,
192 192 related_name='ip+', db_index=True)
193 193
194 194 poster_ip = models.GenericIPAddressField()
195 195 poster_user_agent = models.TextField()
196 196
197 197 last_edit_time = models.DateTimeField()
198 198
199 199 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
200 200 null=True,
201 201 blank=True, related_name='rfp+',
202 202 db_index=True)
203 203 refmap = models.TextField(null=True, blank=True)
204 204 threads = models.ManyToManyField('Thread', db_index=True)
205 205 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
206 206
207 207 def __str__(self):
208 208 return 'P#{}/{}'.format(self.id, self.title)
209 209
210 210 def get_title(self) -> str:
211 211 """
212 212 Gets original post title or part of its text.
213 213 """
214 214
215 215 title = self.title
216 216 if not title:
217 217 title = self.get_text()
218 218
219 219 return title
220 220
221 221 def build_refmap(self) -> None:
222 222 """
223 223 Builds a replies map string from replies list. This is a cache to stop
224 224 the server from recalculating the map on every post show.
225 225 """
226 map_string = ''
227 226
228 227 post_urls = ['<a href="{}">&gt;&gt;{}</a>'.format(
229 refpost.get_url(), refpost.id) for refpost in self.referenced_posts.all()]
228 refpost.get_url(), refpost.id) for refpost in self.referenced_posts.all()]
230 229
231 230 self.refmap = ', '.join(post_urls)
232 231
233 232 def get_sorted_referenced_posts(self):
234 233 return self.refmap
235 234
236 235 def is_referenced(self) -> bool:
237 236 return self.refmap and len(self.refmap) > 0
238 237
239 238 def is_opening(self) -> bool:
240 239 """
241 240 Checks if this is an opening post or just a reply.
242 241 """
243 242
244 243 return self.get_thread().get_opening_post_id() == self.id
245 244
246 @transaction.atomic
247 def add_tag(self, tag):
248 edit_time = timezone.now()
249
250 thread = self.get_thread()
251 thread.add_tag(tag)
252 self.last_edit_time = edit_time
253 self.save(update_fields=['last_edit_time'])
254
255 thread.last_edit_time = edit_time
256 thread.save(update_fields=['last_edit_time'])
257
258 245 @cached_result
259 246 def get_url(self):
260 247 """
261 248 Gets full url to the post.
262 249 """
263 250
264 251 thread = self.get_thread()
265 252
266 253 opening_id = thread.get_opening_post_id()
267 254
268 255 if self.id != opening_id:
269 256 link = reverse('thread', kwargs={
270 257 'post_id': opening_id}) + '#' + str(self.id)
271 258 else:
272 259 link = reverse('thread', kwargs={'post_id': self.id})
273 260
274 261 return link
275 262
276 263 def get_thread(self):
277 264 return self.thread
278 265
279 266 def get_threads(self):
280 267 """
281 268 Gets post's thread.
282 269 """
283 270
284 271 return self.threads
285 272
286 273 def get_referenced_posts(self):
287 274 return self.referenced_posts.only('id', 'threads')
288 275
289 276 def get_view(self, moderator=False, need_open_link=False,
290 277 truncated=False, *args, **kwargs):
291 278 """
292 279 Renders post's HTML view. Some of the post params can be passed over
293 280 kwargs for the means of caching (if we view the thread, some params
294 281 are same for every post and don't need to be computed over and over.
295 282 """
296 283
297 284 thread = self.get_thread()
298 285 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
299 286 can_bump = kwargs.get(PARAMETER_BUMPABLE, thread.can_bump())
300 287
301 288 if is_opening:
302 289 opening_post_id = self.id
303 290 else:
304 291 opening_post_id = thread.get_opening_post_id()
305 292
306 293 return render_to_string('boards/post.html', {
307 294 PARAMETER_POST: self,
308 295 PARAMETER_MODERATOR: moderator,
309 296 PARAMETER_IS_OPENING: is_opening,
310 297 PARAMETER_THREAD: thread,
311 298 PARAMETER_BUMPABLE: can_bump,
312 299 PARAMETER_NEED_OPEN_LINK: need_open_link,
313 300 PARAMETER_TRUNCATED: truncated,
314 301 PARAMETER_OP_ID: opening_post_id,
315 302 })
316 303
317 304 def get_search_view(self, *args, **kwargs):
318 305 return self.get_view(args, kwargs)
319 306
320 307 def get_first_image(self) -> PostImage:
321 308 return self.images.earliest('id')
322 309
323 310 def delete(self, using=None):
324 311 """
325 312 Deletes all post images and the post itself.
326 313 """
327 314
328 315 for image in self.images.all():
329 316 image_refs_count = Post.objects.filter(images__in=[image]).count()
330 317 if image_refs_count == 1:
331 318 image.delete()
332 319
333 320 thread = self.get_thread()
334 321 thread.last_edit_time = timezone.now()
335 322 thread.save()
336 323
337 324 super(Post, self).delete(using)
338 325
339 326 logging.getLogger('boards.post.delete').info(
340 327 'Deleted post {}'.format(self))
341 328
342 329 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
343 330 include_last_update=False):
344 331 """
345 332 Gets post HTML or JSON data that can be rendered on a page or used by
346 333 API.
347 334 """
348 335
349 336 if format_type == DIFF_TYPE_HTML:
350 337 params = dict()
351 338 params['post'] = self
352 339 if PARAMETER_TRUNCATED in request.GET:
353 340 params[PARAMETER_TRUNCATED] = True
354 341 else:
355 342 params[PARAMETER_REPLY_LINK] = True
356 343
357 344 return render_to_string('boards/api_post.html', params)
358 345 elif format_type == DIFF_TYPE_JSON:
359 346 post_json = {
360 347 'id': self.id,
361 348 'title': self.title,
362 349 'text': self._text_rendered,
363 350 }
364 351 if self.images.exists():
365 352 post_image = self.get_first_image()
366 353 post_json['image'] = post_image.image.url
367 354 post_json['image_preview'] = post_image.image.url_200x150
368 355 if include_last_update:
369 356 post_json['bump_time'] = datetime_to_epoch(
370 357 self.get_thread().bump_time)
371 358 return post_json
372 359
373 360 def send_to_websocket(self, request, recursive=True):
374 361 """
375 362 Sends post HTML data to the thread web socket.
376 363 """
377 364
378 365 if not settings.WEBSOCKETS_ENABLED:
379 366 return
380 367
381 368 client = Client()
382 369
383 370 logger = logging.getLogger('boards.post.websocket')
384 371
385 372 thread_ids = list()
386 373 for thread in self.get_threads().all():
387 374 thread_ids.append(thread.id)
388 375
389 376 channel_name = WS_CHANNEL_THREAD + str(thread.get_opening_post_id())
390 377 client.publish(channel_name, {
391 378 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
392 379 })
393 380 client.send()
394 381
395 382 logger.info('Sent notification from post #{} to channel {}'.format(
396 383 self.id, channel_name))
397 384
398 385 if recursive:
399 386 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
400 387 post_id = reply_number.group(1)
401 388
402 389 try:
403 390 ref_post = Post.objects.get(id=post_id)
404 391
405 392 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
406 393 # If post is in this thread, its thread was already notified.
407 394 # Otherwise, notify its thread separately.
408 395 ref_post.send_to_websocket(request, recursive=False)
409 396 except ObjectDoesNotExist:
410 397 pass
411 398
412 399 def save(self, force_insert=False, force_update=False, using=None,
413 400 update_fields=None):
414 401 self._text_rendered = bbcode_extended(self.get_raw_text())
415 402
416 403 super().save(force_insert, force_update, using, update_fields)
417 404
418 405 def get_text(self) -> str:
419 406 return self._text_rendered
420 407
421 408 def get_raw_text(self) -> str:
422 409 return self.text
423 410
424 411 def get_absolute_id(self) -> str:
425 412 """
426 413 If the post has many threads, shows its main thread OP id in the post
427 414 ID.
428 415 """
429 416
430 417 if self.get_threads().count() > 1:
431 418 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
432 419 else:
433 420 return str(self.id)
434 421
435 422 def connect_notifications(self):
436 423 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
437 424 user_name = reply_number.group(1).lower()
438 425 Notification.objects.get_or_create(name=user_name, post=self)
439 426
440 427 def connect_replies(self):
441 428 """
442 429 Connects replies to a post to show them as a reflink map
443 430 """
444 431
445 432 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
446 433 post_id = reply_number.group(1)
447 434
448 435 try:
449 436 referenced_post = Post.objects.get(id=post_id)
450 437
451 438 referenced_post.referenced_posts.add(self)
452 439 referenced_post.last_edit_time = self.pub_time
453 440 referenced_post.build_refmap()
454 441 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
455 442
456 443 referenced_threads = referenced_post.get_threads().all()
457 444 for thread in referenced_threads:
458 445 if thread.can_bump():
459 446 thread.update_bump_status()
460 447
461 448 thread.last_edit_time = self.pub_time
462 449 thread.save(update_fields=['last_edit_time', 'bumpable'])
463 450 except ObjectDoesNotExist:
464 451 pass
465 452
466 453 def connect_threads(self):
467 454 """
468 455 If the referenced post is an OP in another thread,
469 456 make this post multi-thread.
470 457 """
471 458
472 459 for reply_number in re.finditer(REGEX_MULTI_THREAD, self.get_raw_text()):
473 460 post_id = reply_number.group(1)
474 461
475 462 try:
476 463 referenced_post = Post.objects.get(id=post_id)
477 464
478 465 if referenced_post.is_opening():
479 466 referenced_threads = referenced_post.get_threads().all()
480 467 for thread in referenced_threads:
481 468 if thread.can_bump():
482 469 thread.update_bump_status()
483 470
484 471 thread.last_edit_time = self.pub_time
485 472 thread.save(update_fields=['last_edit_time', 'bumpable'])
486 473
487 474 self.threads.add(thread)
488 475 except ObjectDoesNotExist:
489 476 pass
@@ -1,20 +1,21 b''
1 1 {% extends "boards/static_base.html" %}
2 2
3 3 {% load i18n %}
4 4
5 5 {% block head %}
6 6 <title>{% trans "Syntax" %}</title>
7 7 {% endblock %}
8 8
9 9 {% block staticcontent %}
10 10 <h2>{% trans 'Syntax' %}</h2>
11 11 <p>[i]<i>{% trans 'Italic text' %}</i>[/i]</p>
12 12 <p>[b]<b>{% trans 'Bold text' %}</b>[/b]</p>
13 13 <p>[spoiler]<span class="spoiler">{% trans 'Spoiler' %}</span>[/spoiler]</p>
14 <p>[post]123[/post] -- {% trans 'Link to a post' %}</p>
14 <p>[post]123[/post] {% trans 'Link to a post' %}</p>
15 <p>[thread]123[/thread] — {% trans 'Add post to this thread' %}</p>
15 16 <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p>
16 17 <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p>
17 18 <p>[quote]<span class="multiquote">{% trans 'Quote' %}</span>[/quote]</p>
18 19 <br/>
19 20 <p>{% trans 'You can try pasting the text and previewing the result here:' %} <a href="{% url 'preview' %}">{% trans 'Preview' %}</a></p>
20 21 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now