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