##// END OF EJS Templates
Added tree mode for the thread
neko259 -
r1180:e46298a6 default
parent child Browse files
Show More
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0018_banner'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='post',
16 name='referenced_posts',
17 field=models.ManyToManyField(db_index=True, to='boards.Post', null=True, related_name='refposts', blank=True),
18 ),
19 ]
@@ -0,0 +1,19 b''
1 {% extends "boards/thread.html" %}
2
3 {% load i18n %}
4 {% load static from staticfiles %}
5 {% load board %}
6 {% load tz %}
7
8 {% block thread_content %}
9 {% get_current_language as LANGUAGE_CODE %}
10 {% get_current_timezone as TIME_ZONE %}
11
12 <div class="thread">
13 {% for post in thread.get_top_level_replies %}
14 {% post_view post moderator=moderator reply_link=True mode_tree=True %}
15 {% endfor %}
16 </div>
17
18 <script src="{% static 'js/thread.js' %}"></script>
19 {% endblock %}
@@ -0,0 +1,27 b''
1 from boards import settings
2 from boards.views.thread import ThreadView
3
4 TEMPLATE_TREE = 'boards/thread_tree.html'
5
6 CONTEXT_OP = 'opening_post'
7 CONTEXT_BUMPABLE = 'bumpable'
8
9
10 class TreeThreadView(ThreadView):
11
12 def get_template(self):
13 return TEMPLATE_TREE
14
15 def get_data(self, thread):
16 params = dict()
17
18 bumpable = thread.can_bump()
19 params[CONTEXT_BUMPABLE] = bumpable
20 max_posts = thread.max_posts
21
22 params[CONTEXT_OP] = thread.get_opening_post()
23
24 return params
25
26 def get_mode(self):
27 return 'tree'
1 NO CONTENT: modified file, binary diff hidden
@@ -1,427 +1,429 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-05-14 23:38+0300\n"
10 "POT-Creation-Date: 2015-05-19 15:44+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:35
42 42 msgid "Type message here. Use formatting panel for more advanced usage."
43 43 msgstr ""
44 44 "Π’Π²ΠΎΠ΄ΠΈΡ‚Π΅ сообщСниС сюда. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ панСль для Π±ΠΎΠ»Π΅Π΅ слоТного форматирования."
45 45
46 46 #: forms.py:36
47 47 msgid "music images i_dont_like_tags"
48 48 msgstr "ΠΌΡƒΠ·Ρ‹ΠΊΠ° ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ Ρ‚Π΅Π³ΠΈ_Π½Π΅_Π½ΡƒΠΆΠ½Ρ‹"
49 49
50 50 #: forms.py:38
51 51 msgid "Title"
52 52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53 53
54 54 #: forms.py:39
55 55 msgid "Text"
56 56 msgstr "ВСкст"
57 57
58 58 #: forms.py:40
59 59 msgid "Tag"
60 60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
61 61
62 62 #: forms.py:41 templates/boards/base.html:40 templates/search/search.html:7
63 63 msgid "Search"
64 64 msgstr "Поиск"
65 65
66 66 #: forms.py:43
67 67 #, python-format
68 68 msgid "Please wait %s seconds before sending message"
69 69 msgstr "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
70 70
71 71 #: forms.py:144
72 72 msgid "Image"
73 73 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
74 74
75 75 #: forms.py:147
76 76 msgid "Image URL"
77 77 msgstr "URL изобраТСния"
78 78
79 79 #: forms.py:153
80 80 msgid "e-mail"
81 81 msgstr ""
82 82
83 83 #: forms.py:156
84 84 msgid "Additional threads"
85 85 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
86 86
87 87 #: forms.py:167
88 88 #, python-format
89 89 msgid "Title must have less than %s characters"
90 90 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
91 91
92 92 #: forms.py:177
93 93 #, python-format
94 94 msgid "Text must have less than %s characters"
95 95 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
96 96
97 97 #: forms.py:197
98 98 msgid "Invalid URL"
99 99 msgstr "НСвСрный URL"
100 100
101 101 #: forms.py:218
102 102 msgid "Invalid additional thread list"
103 103 msgstr "НСвСрный список Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
104 104
105 105 #: forms.py:250
106 106 msgid "Either text or image must be entered."
107 107 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
108 108
109 109 #: forms.py:288
110 110 #, python-format
111 111 msgid "Image must be less than %s bytes"
112 112 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
113 113
114 114 #: forms.py:335 templates/boards/all_threads.html:129
115 115 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6
116 116 msgid "Tags"
117 117 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ"
118 118
119 119 #: forms.py:342
120 120 msgid "Inappropriate characters in tags."
121 121 msgstr "НСдопустимыС символы Π² ΠΌΠ΅Ρ‚ΠΊΠ°Ρ…."
122 122
123 123 #: forms.py:356
124 124 msgid "Need at least one of the tags: "
125 125 msgstr "НуТна хотя Π±Ρ‹ ΠΎΠ΄Π½Π° ΠΈΠ· ΠΌΠ΅Ρ‚ΠΎΠΊ: "
126 126
127 127 #: forms.py:369
128 128 msgid "Theme"
129 129 msgstr "Π’Π΅ΠΌΠ°"
130 130
131 131 #: forms.py:370
132 132 msgid "Image view mode"
133 133 msgstr "Π Π΅ΠΆΠΈΠΌ просмотра ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
134 134
135 135 #: forms.py:371
136 136 msgid "User name"
137 137 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
138 138
139 139 #: forms.py:372
140 140 msgid "Time zone"
141 141 msgstr "Часовой пояс"
142 142
143 143 #: forms.py:378
144 144 msgid "Inappropriate characters."
145 145 msgstr "НСдопустимыС символы."
146 146
147 147 #: templates/boards/404.html:6
148 148 msgid "Not found"
149 149 msgstr "НС найдСно"
150 150
151 151 #: templates/boards/404.html:12
152 152 msgid "This page does not exist"
153 153 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
154 154
155 155 #: templates/boards/all_threads.html:35
156 156 msgid "Related message"
157 157 msgstr "БвязанноС сообщСниС"
158 158
159 159 #: templates/boards/all_threads.html:60
160 160 msgid "Edit tag"
161 161 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΡƒ"
162 162
163 163 #: templates/boards/all_threads.html:63
164 164 #, python-format
165 165 msgid "This tag has %(thread_count)s threads and %(post_count)s posts."
166 166 msgstr "Π‘ этой ΠΌΠ΅Ρ‚ΠΊΠΎΠΉ Π΅ΡΡ‚ΡŒ %(thread_count)s Ρ‚Π΅ΠΌ ΠΈ %(post_count)s сообщСний."
167 167
168 168 #: templates/boards/all_threads.html:70 templates/boards/feed.html:30
169 169 #: templates/boards/notifications.html:17 templates/search/search.html:26
170 170 msgid "Previous page"
171 171 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
172 172
173 173 #: templates/boards/all_threads.html:84
174 174 #, python-format
175 175 msgid "Skipped %(count)s replies. Open thread to see all replies."
176 176 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
177 177
178 178 #: templates/boards/all_threads.html:102 templates/boards/feed.html:40
179 179 #: templates/boards/notifications.html:27 templates/search/search.html:37
180 180 msgid "Next page"
181 181 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
182 182
183 183 #: templates/boards/all_threads.html:107
184 184 msgid "No threads exist. Create the first one!"
185 185 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
186 186
187 187 #: templates/boards/all_threads.html:113
188 188 msgid "Create new thread"
189 189 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
190 190
191 191 #: templates/boards/all_threads.html:118 templates/boards/preview.html:16
192 #: templates/boards/thread_normal.html:43
192 #: templates/boards/thread_normal.html:38
193 193 msgid "Post"
194 194 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
195 195
196 196 #: templates/boards/all_threads.html:123
197 197 msgid "Tags must be delimited by spaces. Text or image is required."
198 198 msgstr ""
199 199 "ΠœΠ΅Ρ‚ΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
200 200
201 201 #: templates/boards/all_threads.html:126
202 #: templates/boards/thread_normal.html:48
202 #: templates/boards/thread_normal.html:43
203 203 msgid "Text syntax"
204 204 msgstr "Бинтаксис тСкста"
205 205
206 206 #: templates/boards/all_threads.html:143 templates/boards/feed.html:53
207 207 msgid "Pages:"
208 208 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
209 209
210 210 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
211 211 msgid "Authors"
212 212 msgstr "Авторы"
213 213
214 214 #: templates/boards/authors.html:26
215 215 msgid "Distributed under the"
216 216 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
217 217
218 218 #: templates/boards/authors.html:28
219 219 msgid "license"
220 220 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
221 221
222 222 #: templates/boards/authors.html:30
223 223 msgid "Repository"
224 224 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
225 225
226 226 #: templates/boards/base.html:14 templates/boards/base.html.py:41
227 227 msgid "Feed"
228 228 msgstr "Π›Π΅Π½Ρ‚Π°"
229 229
230 230 #: templates/boards/base.html:31
231 231 msgid "All threads"
232 232 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
233 233
234 234 #: templates/boards/base.html:37
235 235 msgid "Add tags"
236 236 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΠΈ"
237 237
238 238 #: templates/boards/base.html:39
239 239 msgid "Tag management"
240 240 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠ°ΠΌΠΈ"
241 241
242 242 #: templates/boards/base.html:39
243 243 msgid "tags"
244 244 msgstr "ΠΌΠ΅Ρ‚ΠΊΠΈ"
245 245
246 246 #: templates/boards/base.html:40
247 #| msgid "Search"
248 247 msgid "search"
249 248 msgstr "поиск"
250 249
251 250 #: templates/boards/base.html:41 templates/boards/feed.html:11
252 251 msgid "feed"
253 252 msgstr "Π»Π΅Π½Ρ‚Π°"
254 253
255 254 #: templates/boards/base.html:44 templates/boards/base.html.py:45
256 255 #: templates/boards/notifications.html:8
257 256 msgid "Notifications"
258 257 msgstr "УвСдомлСния"
259 258
260 259 #: templates/boards/base.html:52 templates/boards/settings.html:8
261 260 msgid "Settings"
262 261 msgstr "Настройки"
263 262
264 263 #: templates/boards/base.html:65
265 264 msgid "Admin"
266 265 msgstr "АдминистрированиС"
267 266
268 267 #: templates/boards/base.html:67
269 268 #, python-format
270 269 msgid "Speed: %(ppd)s posts per day"
271 270 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
272 271
273 272 #: templates/boards/base.html:69
274 273 msgid "Up"
275 274 msgstr "Π’Π²Π΅Ρ€Ρ…"
276 275
277 276 #: templates/boards/feed.html:45
278 277 msgid "No posts exist. Create the first one!"
279 278 msgstr "НСт сообщСний. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΠΎΠ΅!"
280 279
281 280 #: templates/boards/post.html:25
282 281 msgid "Open"
283 282 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
284 283
285 284 #: templates/boards/post.html:27 templates/boards/post.html.py:38
286 285 msgid "Reply"
287 286 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ"
288 287
289 288 #: templates/boards/post.html:33
290 289 msgid " in "
291 290 msgstr " Π² "
292 291
293 292 #: templates/boards/post.html:43
294 293 msgid "Edit"
295 294 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
296 295
297 296 #: templates/boards/post.html:45
298 297 msgid "Edit thread"
299 298 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
300 299
301 #: templates/boards/post.html:77
300 #: templates/boards/post.html:84
302 301 msgid "Replies"
303 302 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
304 303
305 #: templates/boards/post.html:89 templates/boards/thread.html:26
304 #: templates/boards/post.html:97 templates/boards/thread.html:37
306 305 msgid "messages"
307 306 msgstr "сообщСний"
308 307
309 #: templates/boards/post.html:90 templates/boards/thread.html:27
308 #: templates/boards/post.html:98 templates/boards/thread.html:38
310 309 msgid "images"
311 310 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
312 311
313 312 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20
314 313 msgid "Preview"
315 314 msgstr "ΠŸΡ€Π΅Π΄ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
316 315
317 316 #: templates/boards/rss/post.html:5
318 317 msgid "Post image"
319 318 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
320 319
321 320 #: templates/boards/settings.html:16
322 321 msgid "You are moderator."
323 322 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
324 323
325 324 #: templates/boards/settings.html:20
326 325 msgid "Hidden tags:"
327 326 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
328 327
329 328 #: templates/boards/settings.html:28
330 329 msgid "No hidden tags."
331 330 msgstr "НСт скрытых ΠΌΠ΅Ρ‚ΠΎΠΊ."
332 331
333 332 #: templates/boards/settings.html:37
334 333 msgid "Save"
335 334 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
336 335
337 336 #: templates/boards/staticpages/banned.html:6
338 337 msgid "Banned"
339 338 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
340 339
341 340 #: templates/boards/staticpages/banned.html:11
342 341 msgid "Your IP address has been banned. Contact the administrator"
343 342 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
344 343
345 344 #: templates/boards/staticpages/help.html:6
346 345 #: templates/boards/staticpages/help.html:10
347 346 msgid "Syntax"
348 347 msgstr "Бинтаксис"
349 348
350 349 #: templates/boards/staticpages/help.html:11
351 350 msgid "Italic text"
352 351 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
353 352
354 353 #: templates/boards/staticpages/help.html:12
355 354 msgid "Bold text"
356 355 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
357 356
358 357 #: templates/boards/staticpages/help.html:13
359 358 msgid "Spoiler"
360 359 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
361 360
362 361 #: templates/boards/staticpages/help.html:14
363 362 msgid "Link to a post"
364 363 msgstr "Бсылка Π½Π° сообщСниС"
365 364
366 365 #: templates/boards/staticpages/help.html:15
367 366 msgid "Strikethrough text"
368 367 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
369 368
370 369 #: templates/boards/staticpages/help.html:16
371 370 msgid "Comment"
372 371 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
373 372
374 373 #: templates/boards/staticpages/help.html:17
375 374 #: templates/boards/staticpages/help.html:18
376 375 msgid "Quote"
377 376 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
378 377
379 378 #: templates/boards/staticpages/help.html:20
380 379 msgid "You can try pasting the text and previewing the result here:"
381 380 msgstr "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ тСкст ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ здСсь:"
382 381
383 382 #: templates/boards/tags.html:21
384 383 msgid "No tags found."
385 384 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
386 385
387 386 #: templates/boards/tags.html:24
388 387 msgid "All tags"
389 388 msgstr "ВсС ΠΌΠ΅Ρ‚ΠΊΠΈ"
390 389
391 #: templates/boards/thread.html:28
392 msgid "Last update: "
393 msgstr "ПослСднСС обновлСниС: "
394
395 #: templates/boards/thread_gallery.html:19
396 #: templates/boards/thread_normal.html:13
390 #: templates/boards/thread.html:15
397 391 msgid "Normal mode"
398 392 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
399 393
400 #: templates/boards/thread_gallery.html:20
401 #: templates/boards/thread_normal.html:14
394 #: templates/boards/thread.html:16
402 395 msgid "Gallery mode"
403 396 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
404 397
405 #: templates/boards/thread_gallery.html:41
398 #: templates/boards/thread.html:17
399 #| msgid "Normal mode"
400 msgid "Tree mode"
401 msgstr "Π”Ρ€Π΅Π²ΠΎΠ²ΠΈΠ΄Π½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
402
403 #: templates/boards/thread.html:39
404 msgid "Last update: "
405 msgstr "ПослСднСС обновлСниС: "
406
407 #: templates/boards/thread_gallery.html:36
406 408 msgid "No images."
407 409 msgstr "НСт ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ."
408 410
409 #: templates/boards/thread_normal.html:22
411 #: templates/boards/thread_normal.html:17
410 412 msgid "posts to bumplimit"
411 413 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
412 414
413 #: templates/boards/thread_normal.html:36
415 #: templates/boards/thread_normal.html:31
414 416 msgid "Reply to thread"
415 417 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
416 418
417 #: templates/boards/thread_normal.html:49
419 #: templates/boards/thread_normal.html:44
418 420 msgid "Close form"
419 421 msgstr "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ"
420 422
421 #: templates/boards/thread_normal.html:63
423 #: templates/boards/thread_normal.html:58 templates/boards/thread_tree.html:22
422 424 msgid "Update"
423 425 msgstr "ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ"
424 426
425 427 #: templates/search/search.html:17
426 428 msgid "Ok"
427 429 msgstr "Ок"
@@ -1,411 +1,424 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
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.mdx_neboard import Parser
16 16 from boards.models import PostImage
17 17 from boards.models.base import Viewable
18 18 from boards import utils
19 19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
20 20 from boards.models.user import Notification, Ban
21 21 import boards.models.thread
22 22
23 23
24 24 APP_LABEL_BOARDS = 'boards'
25 25
26 26 POSTS_PER_DAY_RANGE = 7
27 27
28 28 BAN_REASON_AUTO = 'Auto'
29 29
30 30 IMAGE_THUMB_SIZE = (200, 150)
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 # TODO This should be removed
35 35 NO_IP = '0.0.0.0'
36 36
37 37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39 39
40 40 PARAMETER_TRUNCATED = 'truncated'
41 41 PARAMETER_TAG = 'tag'
42 42 PARAMETER_OFFSET = 'offset'
43 43 PARAMETER_DIFF_TYPE = 'type'
44 44 PARAMETER_CSS_CLASS = 'css_class'
45 45 PARAMETER_THREAD = 'thread'
46 46 PARAMETER_IS_OPENING = 'is_opening'
47 47 PARAMETER_MODERATOR = 'moderator'
48 48 PARAMETER_POST = 'post'
49 49 PARAMETER_OP_ID = 'opening_post_id'
50 50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 51 PARAMETER_REPLY_LINK = 'reply_link'
52 52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53 53
54 POST_VIEW_PARAMS = (
55 'need_op_data',
56 'reply_link',
57 'moderator',
58 'need_open_link',
59 'truncated',
60 'mode_tree',
61 )
62
54 63 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
55 64
56 65
57 66 class PostManager(models.Manager):
58 67 @transaction.atomic
59 68 def create_post(self, title: str, text: str, image=None, thread=None,
60 69 ip=NO_IP, tags: list=None, threads: list=None):
61 70 """
62 71 Creates new post
63 72 """
64 73
65 74 is_banned = Ban.objects.filter(ip=ip).exists()
66 75
67 76 # TODO Raise specific exception and catch it in the views
68 77 if is_banned:
69 78 raise Exception("This user is banned")
70 79
71 80 if not tags:
72 81 tags = []
73 82 if not threads:
74 83 threads = []
75 84
76 85 posting_time = timezone.now()
77 86 if not thread:
78 87 thread = boards.models.thread.Thread.objects.create(
79 88 bump_time=posting_time, last_edit_time=posting_time)
80 89 new_thread = True
81 90 else:
82 91 new_thread = False
83 92
84 93 pre_text = Parser().preparse(text)
85 94
86 95 post = self.create(title=title,
87 96 text=pre_text,
88 97 pub_time=posting_time,
89 98 poster_ip=ip,
90 99 thread=thread,
91 100 last_edit_time=posting_time)
92 101 post.threads.add(thread)
93 102
94 103 logger = logging.getLogger('boards.post.create')
95 104
96 105 logger.info('Created post {} by {}'.format(post, post.poster_ip))
97 106
98 107 if image:
99 108 post.images.add(PostImage.objects.create_with_hash(image))
100 109
101 110 list(map(thread.add_tag, tags))
102 111
103 112 if new_thread:
104 113 boards.models.thread.Thread.objects.process_oldest_threads()
105 114 else:
106 115 thread.last_edit_time = posting_time
107 116 thread.bump()
108 117 thread.save()
109 118
110 119 post.connect_replies()
111 120 post.connect_threads(threads)
112 121 post.connect_notifications()
113 122
114 123 post.build_url()
115 124
116 125 return post
117 126
118 127 def delete_posts_by_ip(self, ip):
119 128 """
120 129 Deletes all posts of the author with same IP
121 130 """
122 131
123 132 posts = self.filter(poster_ip=ip)
124 133 for post in posts:
125 134 post.delete()
126 135
127 136 @utils.cached_result()
128 137 def get_posts_per_day(self) -> float:
129 138 """
130 139 Gets average count of posts per day for the last 7 days
131 140 """
132 141
133 142 day_end = date.today()
134 143 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
135 144
136 145 day_time_start = timezone.make_aware(datetime.combine(
137 146 day_start, dtime()), timezone.get_current_timezone())
138 147 day_time_end = timezone.make_aware(datetime.combine(
139 148 day_end, dtime()), timezone.get_current_timezone())
140 149
141 150 posts_per_period = float(self.filter(
142 151 pub_time__lte=day_time_end,
143 152 pub_time__gte=day_time_start).count())
144 153
145 154 ppd = posts_per_period / POSTS_PER_DAY_RANGE
146 155
147 156 return ppd
148 157
149 158
150 159 class Post(models.Model, Viewable):
151 160 """A post is a message."""
152 161
153 162 objects = PostManager()
154 163
155 164 class Meta:
156 165 app_label = APP_LABEL_BOARDS
157 166 ordering = ('id',)
158 167
159 168 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
160 169 pub_time = models.DateTimeField()
161 170 text = TextField(blank=True, null=True)
162 171 _text_rendered = TextField(blank=True, null=True, editable=False)
163 172
164 173 images = models.ManyToManyField(PostImage, null=True, blank=True,
165 174 related_name='ip+', db_index=True)
166 175
167 176 poster_ip = models.GenericIPAddressField()
168 177
169 178 # TODO This field can be removed cause UID is used for update now
170 179 last_edit_time = models.DateTimeField()
171 180
172 181 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
173 182 null=True,
174 blank=True, related_name='rfp+',
183 blank=True, related_name='refposts',
175 184 db_index=True)
176 185 refmap = models.TextField(null=True, blank=True)
177 186 threads = models.ManyToManyField('Thread', db_index=True)
178 187 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
179 188
180 189 url = models.TextField()
181 190 uid = models.TextField(db_index=True)
182 191
183 192 def __str__(self):
184 193 return 'P#{}/{}'.format(self.id, self.title)
185 194
195 def get_referenced_posts(self):
196 return self.referenced_posts.order_by('pub_time')
197
186 198 def get_title(self) -> str:
187 199 """
188 200 Gets original post title or part of its text.
189 201 """
190 202
191 203 title = self.title
192 204 if not title:
193 205 title = self.get_text()
194 206
195 207 return title
196 208
197 209 def build_refmap(self) -> None:
198 210 """
199 211 Builds a replies map string from replies list. This is a cache to stop
200 212 the server from recalculating the map on every post show.
201 213 """
202 214
203 215 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
204 216 for refpost in self.referenced_posts.all()]
205 217
206 218 self.refmap = ', '.join(post_urls)
207 219
208 220 def is_referenced(self) -> bool:
209 221 return self.refmap and len(self.refmap) > 0
210 222
211 223 def is_opening(self) -> bool:
212 224 """
213 225 Checks if this is an opening post or just a reply.
214 226 """
215 227
216 228 return self.get_thread().get_opening_post_id() == self.id
217 229
218 230 def get_absolute_url(self):
219 231 return self.url
220 232
221 233 def get_thread(self):
222 234 return self.thread
223 235
224 236 def get_threads(self) -> list:
225 237 """
226 238 Gets post's thread.
227 239 """
228 240
229 241 return self.threads
230 242
231 def get_view(self, moderator=False, need_open_link=False,
232 truncated=False, reply_link=False, *args, **kwargs) -> str:
243 def get_view(self, *args, **kwargs) -> str:
233 244 """
234 245 Renders post's HTML view. Some of the post params can be passed over
235 246 kwargs for the means of caching (if we view the thread, some params
236 247 are same for every post and don't need to be computed over and over.
237 248 """
238 249
239 250 thread = self.get_thread()
240 251 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
241 252
242 253 if is_opening:
243 254 opening_post_id = self.id
244 255 else:
245 256 opening_post_id = thread.get_opening_post_id()
246 257
247 258 css_class = 'post'
248 259 if thread.archived:
249 260 css_class += ' archive_post'
250 261 elif not thread.can_bump():
251 262 css_class += ' dead_post'
252 263
253 return render_to_string('boards/post.html', {
264 params = dict()
265 for param in POST_VIEW_PARAMS:
266 if param in kwargs:
267 params[param] = kwargs[param]
268
269 params.update({
254 270 PARAMETER_POST: self,
255 PARAMETER_MODERATOR: moderator,
256 271 PARAMETER_IS_OPENING: is_opening,
257 272 PARAMETER_THREAD: thread,
258 273 PARAMETER_CSS_CLASS: css_class,
259 PARAMETER_NEED_OPEN_LINK: need_open_link,
260 PARAMETER_TRUNCATED: truncated,
261 274 PARAMETER_OP_ID: opening_post_id,
262 PARAMETER_REPLY_LINK: reply_link,
263 PARAMETER_NEED_OP_DATA: kwargs.get(PARAMETER_NEED_OP_DATA)
264 275 })
265 276
277 return render_to_string('boards/post.html', params)
278
266 279 def get_search_view(self, *args, **kwargs):
267 280 return self.get_view(need_op_data=True, *args, **kwargs)
268 281
269 282 def get_first_image(self) -> PostImage:
270 283 return self.images.earliest('id')
271 284
272 285 def delete(self, using=None):
273 286 """
274 287 Deletes all post images and the post itself.
275 288 """
276 289
277 290 for image in self.images.all():
278 291 image_refs_count = Post.objects.filter(images__in=[image]).count()
279 292 if image_refs_count == 1:
280 293 image.delete()
281 294
282 295 thread = self.get_thread()
283 296 thread.last_edit_time = timezone.now()
284 297 thread.save()
285 298
286 299 super(Post, self).delete(using)
287 300
288 301 logging.getLogger('boards.post.delete').info(
289 302 'Deleted post {}'.format(self))
290 303
291 304 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
292 305 include_last_update=False) -> str:
293 306 """
294 307 Gets post HTML or JSON data that can be rendered on a page or used by
295 308 API.
296 309 """
297 310
298 311 return get_exporter(format_type).export(self, request,
299 312 include_last_update)
300 313
301 314 def notify_clients(self, recursive=True):
302 315 """
303 316 Sends post HTML data to the thread web socket.
304 317 """
305 318
306 319 if not settings.get_bool('External', 'WebsocketsEnabled'):
307 320 return
308 321
309 322 thread_ids = list()
310 323 for thread in self.get_threads().all():
311 324 thread_ids.append(thread.id)
312 325
313 326 thread.notify_clients()
314 327
315 328 if recursive:
316 329 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
317 330 post_id = reply_number.group(1)
318 331
319 332 try:
320 333 ref_post = Post.objects.get(id=post_id)
321 334
322 335 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
323 336 # If post is in this thread, its thread was already notified.
324 337 # Otherwise, notify its thread separately.
325 338 ref_post.notify_clients(recursive=False)
326 339 except ObjectDoesNotExist:
327 340 pass
328 341
329 342 def build_url(self):
330 343 thread = self.get_thread()
331 344 opening_id = thread.get_opening_post_id()
332 345 post_url = reverse('thread', kwargs={'post_id': opening_id})
333 346 if self.id != opening_id:
334 347 post_url += '#' + str(self.id)
335 348 self.url = post_url
336 349 self.save(update_fields=['url'])
337 350
338 351 def save(self, force_insert=False, force_update=False, using=None,
339 352 update_fields=None):
340 353 self._text_rendered = Parser().parse(self.get_raw_text())
341 354
342 355 self.uid = str(uuid.uuid4())
343 356 if update_fields is not None and 'uid' not in update_fields:
344 357 update_fields += ['uid']
345 358
346 359 if self.id:
347 360 for thread in self.get_threads().all():
348 361 if thread.can_bump():
349 362 thread.update_bump_status(exclude_posts=[self])
350 363 thread.last_edit_time = self.last_edit_time
351 364
352 365 thread.save(update_fields=['last_edit_time', 'bumpable'])
353 366
354 367 super().save(force_insert, force_update, using, update_fields)
355 368
356 369 def get_text(self) -> str:
357 370 return self._text_rendered
358 371
359 372 def get_raw_text(self) -> str:
360 373 return self.text
361 374
362 375 def get_absolute_id(self) -> str:
363 376 """
364 377 If the post has many threads, shows its main thread OP id in the post
365 378 ID.
366 379 """
367 380
368 381 if self.get_threads().count() > 1:
369 382 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
370 383 else:
371 384 return str(self.id)
372 385
373 386 def connect_notifications(self):
374 387 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
375 388 user_name = reply_number.group(1).lower()
376 389 Notification.objects.get_or_create(name=user_name, post=self)
377 390
378 391 def connect_replies(self):
379 392 """
380 393 Connects replies to a post to show them as a reflink map
381 394 """
382 395
383 396 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
384 397 post_id = reply_number.group(1)
385 398
386 399 try:
387 400 referenced_post = Post.objects.get(id=post_id)
388 401
389 402 referenced_post.referenced_posts.add(self)
390 403 referenced_post.last_edit_time = self.pub_time
391 404 referenced_post.build_refmap()
392 405 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
393 406 except ObjectDoesNotExist:
394 407 pass
395 408
396 409 def connect_threads(self, opening_posts):
397 410 """
398 411 If the referenced post is an OP in another thread,
399 412 make this post multi-thread.
400 413 """
401 414
402 415 for opening_post in opening_posts:
403 416 threads = opening_post.get_threads().all()
404 417 for thread in threads:
405 418 if thread.can_bump():
406 419 thread.update_bump_status()
407 420
408 421 thread.last_edit_time = self.last_edit_time
409 422 thread.save(update_fields=['last_edit_time', 'bumpable'])
410 423
411 424 self.threads.add(thread)
@@ -1,240 +1,243 b''
1 1 import logging
2 2 from adjacent import Client
3 3
4 4 from django.db.models import Count, Sum
5 5 from django.utils import timezone
6 6 from django.db import models
7 7
8 8 from boards import settings
9 9 import boards
10 10 from boards.utils import cached_result, datetime_to_epoch
11 11 from boards.models.post import Post
12 12 from boards.models.tag import Tag
13 13
14 14
15 15 __author__ = 'neko259'
16 16
17 17
18 18 logger = logging.getLogger(__name__)
19 19
20 20
21 21 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 22 WS_NOTIFICATION_TYPE = 'notification_type'
23 23
24 24 WS_CHANNEL_THREAD = "thread:"
25 25
26 26
27 27 class ThreadManager(models.Manager):
28 28 def process_oldest_threads(self):
29 29 """
30 30 Preserves maximum thread count. If there are too many threads,
31 31 archive or delete the old ones.
32 32 """
33 33
34 34 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
35 35 thread_count = threads.count()
36 36
37 37 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
38 38 if thread_count > max_thread_count:
39 39 num_threads_to_delete = thread_count - max_thread_count
40 40 old_threads = threads[thread_count - num_threads_to_delete:]
41 41
42 42 for thread in old_threads:
43 43 if settings.get_bool('Storage', 'ArchiveThreads'):
44 44 self._archive_thread(thread)
45 45 else:
46 46 thread.delete()
47 47
48 48 logger.info('Processed %d old threads' % num_threads_to_delete)
49 49
50 50 def _archive_thread(self, thread):
51 51 thread.archived = True
52 52 thread.bumpable = False
53 53 thread.last_edit_time = timezone.now()
54 54 thread.update_posts_time()
55 55 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
56 56
57 57
58 58 def get_thread_max_posts():
59 59 return settings.get_int('Messages', 'MaxPostsPerThread')
60 60
61 61
62 62 class Thread(models.Model):
63 63 objects = ThreadManager()
64 64
65 65 class Meta:
66 66 app_label = 'boards'
67 67
68 68 tags = models.ManyToManyField('Tag')
69 69 bump_time = models.DateTimeField(db_index=True)
70 70 last_edit_time = models.DateTimeField()
71 71 archived = models.BooleanField(default=False)
72 72 bumpable = models.BooleanField(default=True)
73 73 max_posts = models.IntegerField(default=get_thread_max_posts)
74 74
75 75 def get_tags(self) -> list:
76 76 """
77 77 Gets a sorted tag list.
78 78 """
79 79
80 80 return self.tags.order_by('name')
81 81
82 82 def bump(self):
83 83 """
84 84 Bumps (moves to up) thread if possible.
85 85 """
86 86
87 87 if self.can_bump():
88 88 self.bump_time = self.last_edit_time
89 89
90 90 self.update_bump_status()
91 91
92 92 logger.info('Bumped thread %d' % self.id)
93 93
94 94 def has_post_limit(self) -> bool:
95 95 return self.max_posts > 0
96 96
97 97 def update_bump_status(self, exclude_posts=None):
98 98 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
99 99 self.bumpable = False
100 100 self.update_posts_time(exclude_posts=exclude_posts)
101 101
102 102 def _get_cache_key(self):
103 103 return [datetime_to_epoch(self.last_edit_time)]
104 104
105 105 @cached_result(key_method=_get_cache_key)
106 106 def get_reply_count(self) -> int:
107 107 return self.get_replies().count()
108 108
109 109 @cached_result(key_method=_get_cache_key)
110 110 def get_images_count(self) -> int:
111 111 return self.get_replies().annotate(images_count=Count(
112 112 'images')).aggregate(Sum('images_count'))['images_count__sum']
113 113
114 114 def can_bump(self) -> bool:
115 115 """
116 116 Checks if the thread can be bumped by replying to it.
117 117 """
118 118
119 119 return self.bumpable and not self.archived
120 120
121 121 def get_last_replies(self) -> list:
122 122 """
123 123 Gets several last replies, not including opening post
124 124 """
125 125
126 126 last_replies_count = settings.get_int('View', 'LastRepliesCount')
127 127
128 128 if last_replies_count > 0:
129 129 reply_count = self.get_reply_count()
130 130
131 131 if reply_count > 0:
132 132 reply_count_to_show = min(last_replies_count,
133 133 reply_count - 1)
134 134 replies = self.get_replies()
135 135 last_replies = replies[reply_count - reply_count_to_show:]
136 136
137 137 return last_replies
138 138
139 139 def get_skipped_replies_count(self) -> int:
140 140 """
141 141 Gets number of posts between opening post and last replies.
142 142 """
143 143 reply_count = self.get_reply_count()
144 144 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
145 145 reply_count - 1)
146 146 return reply_count - last_replies_count - 1
147 147
148 148 def get_replies(self, view_fields_only=False) -> list:
149 149 """
150 150 Gets sorted thread posts
151 151 """
152 152
153 153 query = Post.objects.filter(threads__in=[self])
154 154 query = query.order_by('pub_time').prefetch_related('images', 'thread', 'threads')
155 155 if view_fields_only:
156 156 query = query.defer('poster_ip')
157 157 return query.all()
158 158
159 def get_top_level_replies(self):
160 return self.get_replies().exclude(refposts__threads__in=[self])
161
159 162 def get_replies_with_images(self, view_fields_only=False) -> list:
160 163 """
161 164 Gets replies that have at least one image attached
162 165 """
163 166
164 167 return self.get_replies(view_fields_only).annotate(images_count=Count(
165 168 'images')).filter(images_count__gt=0)
166 169
167 170 # TODO Do we still need this?
168 171 def add_tag(self, tag: Tag):
169 172 """
170 173 Connects thread to a tag and tag to a thread
171 174 """
172 175
173 176 self.tags.add(tag)
174 177
175 178 def get_opening_post(self, only_id=False) -> Post:
176 179 """
177 180 Gets the first post of the thread
178 181 """
179 182
180 183 query = self.get_replies().order_by('pub_time')
181 184 if only_id:
182 185 query = query.only('id')
183 186 opening_post = query.first()
184 187
185 188 return opening_post
186 189
187 190 @cached_result()
188 191 def get_opening_post_id(self) -> int:
189 192 """
190 193 Gets ID of the first thread post.
191 194 """
192 195
193 196 return self.get_opening_post(only_id=True).id
194 197
195 198 def get_pub_time(self):
196 199 """
197 200 Gets opening post's pub time because thread does not have its own one.
198 201 """
199 202
200 203 return self.get_opening_post().pub_time
201 204
202 205 def delete(self, using=None):
203 206 """
204 207 Deletes thread with all replies.
205 208 """
206 209
207 210 for reply in self.get_replies().all():
208 211 reply.delete()
209 212
210 213 super(Thread, self).delete(using)
211 214
212 215 def __str__(self):
213 216 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
214 217
215 218 def get_tag_url_list(self) -> list:
216 219 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
217 220
218 221 def update_posts_time(self, exclude_posts=None):
219 222 for post in self.post_set.all():
220 223 if exclude_posts is not None and post not in exclude_posts:
221 224 # Manual update is required because uids are generated on save
222 225 post.last_edit_time = self.last_edit_time
223 226 post.save(update_fields=['last_edit_time'])
224 227
225 228 post.threads.update(last_edit_time=self.last_edit_time)
226 229
227 230 def notify_clients(self):
228 231 if not settings.get_bool('External', 'WebsocketsEnabled'):
229 232 return
230 233
231 234 client = Client()
232 235
233 236 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
234 237 client.publish(channel_name, {
235 238 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
236 239 })
237 240 client.send()
238 241
239 242 def get_absolute_url(self):
240 243 return self.get_opening_post().get_absolute_url()
@@ -1,526 +1,533 b''
1 1 * {
2 2 text-decoration: none;
3 3 font-weight: inherit;
4 4 }
5 5
6 6 b, strong {
7 7 font-weight: bold;
8 8 }
9 9
10 10 html {
11 11 background: #555;
12 12 color: #ffffff;
13 13 }
14 14
15 15 body {
16 16 margin: 0;
17 17 }
18 18
19 19 #admin_panel {
20 20 background: #FF0000;
21 21 color: #00FF00
22 22 }
23 23
24 24 .input_field_error {
25 25 color: #FF0000;
26 26 }
27 27
28 28 .title {
29 29 font-weight: bold;
30 30 color: #ffcc00;
31 31 }
32 32
33 33 .link, a {
34 34 color: #afdcec;
35 35 }
36 36
37 37 .block {
38 38 display: inline-block;
39 39 vertical-align: top;
40 40 }
41 41
42 42 .tag {
43 43 color: #FFD37D;
44 44 }
45 45
46 46 .post_id {
47 47 color: #fff380;
48 48 }
49 49
50 50 .post, .dead_post, .archive_post, #posts-table {
51 51 background: #333;
52 52 padding: 10px;
53 53 clear: left;
54 54 word-wrap: break-word;
55 55 border-top: 1px solid #777;
56 56 border-bottom: 1px solid #777;
57 57 }
58 58
59 59 .post + .post {
60 60 border-top: none;
61 61 }
62 62
63 63 .dead_post + .dead_post {
64 64 border-top: none;
65 65 }
66 66
67 67 .archive_post + .archive_post {
68 68 border-top: none;
69 69 }
70 70
71 71 .metadata {
72 72 padding-top: 5px;
73 73 margin-top: 10px;
74 74 border-top: solid 1px #666;
75 75 color: #ddd;
76 76 }
77 77
78 78 .navigation_panel, .tag_info {
79 79 background: #222;
80 80 margin-bottom: 5px;
81 81 margin-top: 5px;
82 82 padding: 10px;
83 83 border-bottom: solid 1px #888;
84 84 border-top: solid 1px #888;
85 85 color: #eee;
86 86 }
87 87
88 88 .navigation_panel .link:first-child {
89 89 border-right: 1px solid #fff;
90 90 font-weight: bold;
91 91 margin-right: 1ex;
92 92 padding-right: 1ex;
93 93 }
94 94
95 95 .navigation_panel .right-link {
96 96 border-left: 1px solid #fff;
97 97 border-right: none;
98 98 float: right;
99 99 margin-left: 1ex;
100 100 margin-right: 0;
101 101 padding-left: 1ex;
102 102 padding-right: 0;
103 103 }
104 104
105 105 .navigation_panel .link {
106 106 font-weight: bold;
107 107 }
108 108
109 109 .navigation_panel::after, .post::after {
110 110 clear: both;
111 111 content: ".";
112 112 display: block;
113 113 height: 0;
114 114 line-height: 0;
115 115 visibility: hidden;
116 116 }
117 117
118 118 .header {
119 119 border-bottom: solid 2px #ccc;
120 120 margin-bottom: 5px;
121 121 border-top: none;
122 122 margin-top: 0;
123 123 }
124 124
125 125 .footer {
126 126 border-top: solid 2px #ccc;
127 127 margin-top: 5px;
128 128 border-bottom: none;
129 129 margin-bottom: 0;
130 130 }
131 131
132 132 p, .br {
133 133 margin-top: .5em;
134 134 margin-bottom: .5em;
135 135 }
136 136
137 137 .post-form-w {
138 138 background: #333344;
139 139 border-top: solid 1px #888;
140 140 border-bottom: solid 1px #888;
141 141 color: #fff;
142 142 padding: 10px;
143 143 margin-bottom: 5px;
144 144 margin-top: 5px;
145 145 }
146 146
147 147 .form-row {
148 148 width: 100%;
149 149 display: table-row;
150 150 }
151 151
152 152 .form-label {
153 153 padding: .25em 1ex .25em 0;
154 154 vertical-align: top;
155 155 display: table-cell;
156 156 }
157 157
158 158 .form-input {
159 159 padding: .25em 0;
160 160 width: 100%;
161 161 display: table-cell;
162 162 }
163 163
164 164 .form-errors {
165 165 font-weight: bolder;
166 166 vertical-align: middle;
167 167 display: table-cell;
168 168 }
169 169
170 170 .post-form input:not([name="image"]), .post-form textarea, .post-form select {
171 171 background: #333;
172 172 color: #fff;
173 173 border: solid 1px;
174 174 padding: 0;
175 175 font: medium sans-serif;
176 176 width: 100%;
177 177 }
178 178
179 179 .post-form textarea {
180 180 resize: vertical;
181 181 }
182 182
183 183 .form-submit {
184 184 display: table;
185 185 margin-bottom: 1ex;
186 186 margin-top: 1ex;
187 187 }
188 188
189 189 .form-title {
190 190 font-weight: bold;
191 191 font-size: 2ex;
192 192 margin-bottom: 0.5ex;
193 193 }
194 194
195 195 .post-form input[type="submit"], input[type="submit"] {
196 196 background: #222;
197 197 border: solid 2px #fff;
198 198 color: #fff;
199 199 padding: 0.5ex;
200 200 }
201 201
202 202 input[type="submit"]:hover {
203 203 background: #060;
204 204 }
205 205
206 206 blockquote {
207 207 border-left: solid 2px;
208 208 padding-left: 5px;
209 209 color: #B1FB17;
210 210 margin: 0;
211 211 }
212 212
213 213 .post > .image {
214 214 float: left;
215 215 margin: 0 1ex .5ex 0;
216 216 min-width: 1px;
217 217 text-align: center;
218 218 display: table-row;
219 219 }
220 220
221 221 .post > .metadata {
222 222 clear: left;
223 223 }
224 224
225 225 .get {
226 226 font-weight: bold;
227 227 color: #d55;
228 228 }
229 229
230 230 * {
231 231 text-decoration: none;
232 232 }
233 233
234 234 .dead_post {
235 235 background-color: #442222;
236 236 }
237 237
238 238 .archive_post {
239 239 background-color: #000;
240 240 }
241 241
242 242 .mark_btn {
243 243 border: 1px solid;
244 244 padding: 2px 2ex;
245 245 display: inline-block;
246 246 margin: 0 5px 4px 0;
247 247 }
248 248
249 249 .mark_btn:hover {
250 250 background: #555;
251 251 }
252 252
253 253 .quote {
254 254 color: #92cf38;
255 255 font-style: italic;
256 256 }
257 257
258 258 .multiquote {
259 259 padding: 3px;
260 260 display: inline-block;
261 261 background: #222;
262 262 border-style: solid;
263 263 border-width: 1px 1px 1px 4px;
264 264 font-size: 0.9em;
265 265 }
266 266
267 267 .spoiler {
268 268 background: black;
269 269 color: black;
270 270 padding: 0 1ex 0 1ex;
271 271 }
272 272
273 273 .spoiler:hover {
274 274 color: #ddd;
275 275 }
276 276
277 277 .comment {
278 278 color: #eb2;
279 279 }
280 280
281 281 a:hover {
282 282 text-decoration: underline;
283 283 }
284 284
285 285 .last-replies {
286 286 margin-left: 3ex;
287 287 margin-right: 3ex;
288 288 border-left: solid 1px #777;
289 289 border-right: solid 1px #777;
290 290 }
291 291
292 292 .last-replies > .post:first-child {
293 293 border-top: none;
294 294 }
295 295
296 296 .thread {
297 297 margin-bottom: 3ex;
298 298 margin-top: 1ex;
299 299 }
300 300
301 301 .post:target {
302 302 border: solid 2px white;
303 303 }
304 304
305 305 pre{
306 306 white-space:pre-wrap
307 307 }
308 308
309 309 li {
310 310 list-style-position: inside;
311 311 }
312 312
313 313 .fancybox-skin {
314 314 position: relative;
315 315 background-color: #fff;
316 316 color: #ddd;
317 317 text-shadow: none;
318 318 }
319 319
320 320 .fancybox-image {
321 321 border: 1px solid black;
322 322 }
323 323
324 324 .image-mode-tab {
325 325 background: #444;
326 326 color: #eee;
327 327 margin-top: 5px;
328 328 padding: 5px;
329 329 border-top: 1px solid #888;
330 330 border-bottom: 1px solid #888;
331 331 }
332 332
333 333 .image-mode-tab > label {
334 334 margin: 0 1ex;
335 335 }
336 336
337 337 .image-mode-tab > label > input {
338 338 margin-right: .5ex;
339 339 }
340 340
341 341 #posts-table {
342 342 margin-top: 5px;
343 343 margin-bottom: 5px;
344 344 }
345 345
346 346 .tag_info > h2 {
347 347 margin: 0;
348 348 }
349 349
350 350 .post-info {
351 351 color: #ddd;
352 352 margin-bottom: 1ex;
353 353 }
354 354
355 355 .moderator_info {
356 356 color: #e99d41;
357 357 float: right;
358 358 font-weight: bold;
359 359 opacity: 0.4;
360 360 }
361 361
362 362 .moderator_info:hover {
363 363 opacity: 1;
364 364 }
365 365
366 366 .refmap {
367 367 font-size: 0.9em;
368 368 color: #ccc;
369 369 margin-top: 1em;
370 370 }
371 371
372 372 .fav {
373 373 color: yellow;
374 374 }
375 375
376 376 .not_fav {
377 377 color: #ccc;
378 378 }
379 379
380 380 .role {
381 381 text-decoration: underline;
382 382 }
383 383
384 384 .form-email {
385 385 display: none;
386 386 }
387 387
388 388 .bar-value {
389 389 background: rgba(50, 55, 164, 0.45);
390 390 font-size: 0.9em;
391 391 height: 1.5em;
392 392 }
393 393
394 394 .bar-bg {
395 395 position: relative;
396 396 border-top: solid 1px #888;
397 397 border-bottom: solid 1px #888;
398 398 margin-top: 5px;
399 399 overflow: hidden;
400 400 }
401 401
402 402 .bar-text {
403 403 padding: 2px;
404 404 position: absolute;
405 405 left: 0;
406 406 top: 0;
407 407 }
408 408
409 409 .page_link {
410 410 background: #444;
411 411 border-top: solid 1px #888;
412 412 border-bottom: solid 1px #888;
413 413 padding: 5px;
414 414 color: #eee;
415 415 font-size: 2ex;
416 416 }
417 417
418 418 .skipped_replies {
419 419 padding: 5px;
420 420 margin-left: 3ex;
421 421 margin-right: 3ex;
422 422 border-left: solid 1px #888;
423 423 border-right: solid 1px #888;
424 424 border-bottom: solid 1px #888;
425 425 background: #000;
426 426 }
427 427
428 428 .current_page {
429 429 padding: 2px;
430 430 background-color: #afdcec;
431 431 color: #000;
432 432 }
433 433
434 434 .current_mode {
435 435 font-weight: bold;
436 436 }
437 437
438 438 .gallery_image {
439 439 border: solid 1px;
440 440 padding: 0.5ex;
441 441 margin: 0.5ex;
442 442 text-align: center;
443 443 }
444 444
445 445 code {
446 446 border: dashed 1px #ccc;
447 447 background: #111;
448 448 padding: 2px;
449 449 font-size: 1.2em;
450 450 display: inline-block;
451 451 }
452 452
453 453 pre {
454 454 overflow: auto;
455 455 }
456 456
457 457 .img-full {
458 458 background: #222;
459 459 border: solid 1px white;
460 460 }
461 461
462 462 .tag_item {
463 463 display: inline-block;
464 464 }
465 465
466 466 #id_models li {
467 467 list-style: none;
468 468 }
469 469
470 470 #id_q {
471 471 margin-left: 1ex;
472 472 }
473 473
474 474 ul {
475 475 padding-left: 0px;
476 476 }
477 477
478 478 .quote-header {
479 479 border-bottom: 2px solid #ddd;
480 480 margin-bottom: 1ex;
481 481 padding-bottom: .5ex;
482 482 color: #ddd;
483 483 font-size: 1.2em;
484 484 }
485 485
486 486 /* Reflink preview */
487 487 .post_preview {
488 488 border-left: 1px solid #777;
489 489 border-right: 1px solid #777;
490 490 max-width: 600px;
491 491 }
492 492
493 493 /* Code highlighter */
494 494 .hljs {
495 495 color: #fff;
496 496 background: #000;
497 497 display: inline-block;
498 498 }
499 499
500 500 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
501 501 color: #fff;
502 502 }
503 503
504 504 #up {
505 505 position: fixed;
506 506 bottom: 5px;
507 507 right: 5px;
508 508 border: 1px solid #777;
509 509 background: #000;
510 510 padding: 4px;
511 511 }
512 512
513 513 .user-cast {
514 514 border: solid #ffffff 1px;
515 515 padding: .2ex;
516 516 background: #152154;
517 517 color: #fff;
518 518 }
519 519
520 520 .highlight {
521 521 background-color: #222;
522 522 }
523 523
524 524 .post-button-form > button:hover {
525 525 text-decoration: underline;
526 526 }
527
528 .tree_reply > .post {
529 margin-left: 1ex;
530 margin-top: 1ex;
531 border-left: solid 1px #777;
532 border-right: solid 1px #777;
533 }
@@ -1,95 +1,103 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 {% comment %}
12 12 Thread death time needs to be shown only if the thread is alredy archived
13 13 and this is an opening post (thread death time) or a post for popup
14 14 (we don't see OP here so we show the death time in the post itself).
15 15 {% endcomment %}
16 16 {% if thread.archived %}
17 17 {% if is_opening %}
18 18 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
19 19 {% endif %}
20 20 {% endif %}
21 21 {% if is_opening %}
22 22 {% if need_open_link %}
23 23 {% if thread.archived %}
24 24 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
25 25 {% else %}
26 26 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
27 27 {% endif %}
28 28 {% endif %}
29 29 {% else %}
30 30 {% if need_op_data %}
31 31 {% with thread.get_opening_post as op %}
32 32 {% trans " in " %}<a href="{{ op.get_absolute_url }}">&gt;&gt;{{ op.id }}</a> <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
33 33 {% endwith %}
34 34 {% endif %}
35 35 {% endif %}
36 36 {% if reply_link and not thread.archived %}
37 37 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
38 38 {% endif %}
39 39
40 40 {% if moderator %}
41 41 <span class="moderator_info">
42 42 <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
43 43 {% if is_opening %}
44 44 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
45 45 {% endif %}
46 46 </span>
47 47 {% endif %}
48 48 </div>
49 49 {% comment %}
50 50 Post images. Currently only 1 image can be posted and shown, but post model
51 51 supports multiple.
52 52 {% endcomment %}
53 53 {% if post.images.exists %}
54 54 {% with post.images.all.0 as image %}
55 55 {% autoescape off %}
56 56 {{ image.get_view }}
57 57 {% endautoescape %}
58 58 {% endwith %}
59 59 {% endif %}
60 60 {% comment %}
61 61 Post message (text)
62 62 {% endcomment %}
63 63 <div class="message">
64 64 {% autoescape off %}
65 65 {% if truncated %}
66 66 {{ post.get_text|truncatewords_html:50 }}
67 67 {% else %}
68 68 {{ post.get_text }}
69 69 {% endif %}
70 70 {% endautoescape %}
71 71 {% if post.is_referenced %}
72 <div class="refmap">
73 {% autoescape off %}
74 {% trans "Replies" %}: {{ post.refmap }}
75 {% endautoescape %}
76 </div>
72 {% if mode_tree %}
73 <div class="tree_reply">
74 {% for refpost in post.get_referenced_posts %}
75 {% post_view refpost mode_tree=True %}
76 {% endfor %}
77 </div>
78 {% else %}
79 <div class="refmap">
80 {% autoescape off %}
81 {% trans "Replies" %}: {{ post.refmap }}
82 {% endautoescape %}
83 </div>
84 {% endif %}
77 85 {% endif %}
78 86 </div>
79 87 {% comment %}
80 88 Thread metadata: counters, tags etc
81 89 {% endcomment %}
82 90 {% if is_opening %}
83 91 <div class="metadata">
84 92 {% if is_opening and need_open_link %}
85 93 {{ thread.get_reply_count }} {% trans 'messages' %},
86 94 {{ thread.get_images_count }} {% trans 'images' %}.
87 95 {% endif %}
88 96 <span class="tags">
89 97 {% autoescape off %}
90 98 {{ thread.get_tag_url_list }}
91 99 {% endautoescape %}
92 100 </span>
93 101 </div>
94 102 {% endif %}
95 103 </div>
@@ -1,32 +1,43 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load static from staticfiles %}
5 5 {% load board %}
6 6 {% load tz %}
7 7
8 8 {% block head %}
9 9 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
10 10 - {{ site_name }}</title>
11 11 {% endblock %}
12 12
13 {% block content %}
14 <div class="image-mode-tab">
15 <a {% ifequal mode 'normal' %}class="current_mode"{% endifequal %} href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
16 <a {% ifequal mode 'gallery' %}class="current_mode"{% endifequal %} href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery mode' %}</a>,
17 <a {% ifequal mode 'tree' %}class="current_mode"{% endifequal %} href="{% url 'thread_tree' opening_post.id %}">{% trans 'Tree mode' %}</a>
18 </div>
19
20 {% block thread_content %}
21 {% endblock %}
22 {% endblock %}
23
13 24 {% block metapanel %}
14 25
15 26 <span class="metapanel"
16 27 data-last-update="{{ last_update }}"
17 28 data-ws-token-time="{{ ws_token_time }}"
18 29 data-ws-token="{{ ws_token }}"
19 30 data-ws-project="{{ ws_project }}"
20 31 data-ws-host="{{ ws_host }}"
21 32 data-ws-port="{{ ws_port }}">
22 33
23 34 {% block thread_meta_panel %}
24 35 {% endblock %}
25 36
26 37 <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} {% trans 'messages' %},
27 38 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
28 39 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span>
29 40 [<a href="rss/">RSS</a>]
30 41 </span>
31 42
32 43 {% endblock %}
@@ -1,44 +1,39 b''
1 1 {% extends "boards/thread.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load static from staticfiles %}
5 5 {% load board %}
6 6 {% load tz %}
7 7
8 8 {% block head %}
9 9 <meta name="robots" content="noindex">
10 10 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }}
11 11 - {{ site_name }}</title>
12 12 {% endblock %}
13 13
14 {% block content %}
14 {% block thread_content %}
15 15 {% get_current_language as LANGUAGE_CODE %}
16 16 {% get_current_timezone as TIME_ZONE %}
17 17
18 <div class="image-mode-tab">
19 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
20 <a class="current_mode" href="{% url 'thread_gallery' thread.get_opening_post.id %}">{% trans 'Gallery mode' %}</a>
21 </div>
22
23 18 <div id="posts-table">
24 19 {% if posts %}
25 20 {% for post in posts %}
26 21 <div class="gallery_image">
27 22 {% with post.get_first_image as image %}
28 23 {% autoescape off %}
29 24 {{ image.get_view }}
30 25 {% endautoescape %}
31 26 <div class="gallery_image_metadata">
32 27 {{ image.width }}x{{ image.height }}
33 28 {% image_actions image.image.url request.get_host %}
34 29 <br />
35 30 <a href="{{ post.get_absolute_url }}">>>{{ post.id }}</a>
36 31 </div>
37 32 {% endwith %}
38 33 </div>
39 34 {% endfor %}
40 35 {% else %}
41 36 {% trans 'No images.' %}
42 37 {% endif %}
43 38 </div>
44 39 {% endblock %}
@@ -1,64 +1,59 b''
1 1 {% extends "boards/thread.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load static from staticfiles %}
5 5 {% load board %}
6 6 {% load tz %}
7 7
8 {% block content %}
8 {% block thread_content %}
9 9 {% get_current_language as LANGUAGE_CODE %}
10 10 {% get_current_timezone as TIME_ZONE %}
11 11
12 <div class="image-mode-tab">
13 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
14 <a href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery mode' %}</a>
15 </div>
16
17 12 {% if bumpable and thread.has_post_limit %}
18 13 <div class="bar-bg">
19 14 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
20 15 </div>
21 16 <div class="bar-text">
22 17 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
23 18 </div>
24 19 </div>
25 20 {% endif %}
26 21
27 22 <div class="thread">
28 23 {% for post in thread.get_replies %}
29 24 {% post_view post moderator=moderator reply_link=True %}
30 25 {% endfor %}
31 26 </div>
32 27
33 28 {% if not thread.archived %}
34 29 <div class="post-form-w">
35 30 <script src="{% static 'js/panel.js' %}"></script>
36 31 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
37 32 <div class="post-form" id="compact-form">
38 33 <div class="swappable-form-full">
39 34 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
40 35 <div class="compact-form-text"></div>
41 36 {{ form.as_div }}
42 37 <div class="form-submit">
43 38 <input type="submit" value="{% trans "Post" %}"/>
44 39 </div>
45 40 </form>
46 41 </div>
47 42 <div><a href="{% url "staticpage" name="help" %}">
48 43 {% trans 'Text syntax' %}</a></div>
49 44 <div><a id="form-close-button" href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
50 45 </div>
51 46 </div>
52 47
53 48 <script src="{% static 'js/jquery.form.min.js' %}"></script>
54 49 {% endif %}
55 50
56 51 <script src="{% static 'js/form.js' %}"></script>
57 52 <script src="{% static 'js/thread.js' %}"></script>
58 53 <script src="{% static 'js/thread_update.js' %}"></script>
59 54 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
60 55 {% endblock %}
61 56
62 57 {% block thread_meta_panel %}
63 58 <button id="autoupdate">{% trans 'Update' %}</button>
64 59 {% endblock %}
@@ -1,85 +1,87 b''
1 1 from django.conf.urls import patterns, url
2 2 from django.views.i18n import javascript_catalog
3 3
4 4 from boards import views
5 5 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
6 6 from boards.views import api, tag_threads, all_threads, \
7 7 settings, all_tags, feed
8 8 from boards.views.authors import AuthorsView
9 9 from boards.views.notifications import NotificationView
10 10 from boards.views.search import BoardSearchView
11 11 from boards.views.static import StaticPageView
12 12 from boards.views.preview import PostPreviewView
13 13
14 14
15 15 js_info_dict = {
16 16 'packages': ('boards',),
17 17 }
18 18
19 19 urlpatterns = patterns('',
20 20 # /boards/
21 21 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
22 22 # /boards/page/
23 23 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
24 24 name='index'),
25 25
26 26 # /boards/tag/tag_name/
27 27 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
28 28 name='tag'),
29 29 # /boards/tag/tag_id/page/
30 30 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
31 31 tag_threads.TagView.as_view(), name='tag'),
32 32
33 33 # /boards/thread/
34 url(r'^thread/(?P<post_id>\d+)/$', views.thread.normal.NormalThreadView.as_view(),
34 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
35 35 name='thread'),
36 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.gallery.GalleryThreadView.as_view(),
36 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
37 37 name='thread_gallery'),
38 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
39 name='thread_tree'),
38 40 # /feed/
39 41 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
40 42 url(r'^feed/page/(?P<page>\w+)/$', views.feed.FeedView.as_view(),
41 43 name='feed'),
42 44
43 45 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
44 46 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
45 47 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
46 48
47 49 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
48 50 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
49 51 name='staticpage'),
50 52
51 53 # RSS feeds
52 54 url(r'^rss/$', AllThreadsFeed()),
53 55 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
54 56 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
55 57 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
56 58 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
57 59
58 60 # i18n
59 61 url(r'^jsi18n/$', javascript_catalog, js_info_dict,
60 62 name='js_info_dict'),
61 63
62 64 # API
63 65 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
64 66 url(r'^api/diff_thread$',
65 67 api.api_get_threaddiff, name="get_thread_diff"),
66 68 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
67 69 name='get_threads'),
68 70 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
69 71 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
70 72 name='get_thread'),
71 73 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
72 74 name='add_post'),
73 75 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
74 76 name='api_notifications'),
75 77
76 78 # Search
77 79 url(r'^search/$', BoardSearchView.as_view(), name='search'),
78 80
79 81 # Notifications
80 82 url(r'^notifications/(?P<username>\w+)$', NotificationView.as_view(), name='notifications'),
81 83
82 84 # Post preview
83 85 url(r'^preview/$', PostPreviewView.as_view(), name='preview')
84 86
85 87 )
@@ -1,3 +1,4 b''
1 1 from boards.views.thread.thread import ThreadView
2 2 from boards.views.thread.normal import NormalThreadView
3 3 from boards.views.thread.gallery import GalleryThreadView
4 from boards.views.thread.tree import TreeThreadView
@@ -1,19 +1,24 b''
1 1 from boards.views.thread import ThreadView
2 2
3 3 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
4 4
5 5 CONTEXT_POSTS = 'posts'
6 CONTEXT_OP = 'opening_post'
6 7
7 8
8 9 class GalleryThreadView(ThreadView):
9 10
10 11 def get_template(self):
11 12 return TEMPLATE_GALLERY
12 13
13 14 def get_data(self, thread):
14 15 params = dict()
15 16
16 17 params[CONTEXT_POSTS] = thread.get_replies_with_images(
17 18 view_fields_only=True)
19 params[CONTEXT_OP] = thread.get_opening_post()
18 20
19 21 return params
22
23 def get_mode(self):
24 return 'gallery'
@@ -1,31 +1,34 b''
1 1 from boards import settings
2 2 from boards.views.thread import ThreadView
3 3
4 4 TEMPLATE_NORMAL = 'boards/thread_normal.html'
5 5
6 6 CONTEXT_OP = 'opening_post'
7 7 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
8 8 CONTEXT_POSTS_LEFT = 'posts_left'
9 9 CONTEXT_BUMPABLE = 'bumpable'
10 10
11 11
12 12 class NormalThreadView(ThreadView):
13 13
14 14 def get_template(self):
15 15 return TEMPLATE_NORMAL
16 16
17 def get_mode(self):
18 return 'normal'
19
17 20 def get_data(self, thread):
18 21 params = dict()
19 22
20 23 bumpable = thread.can_bump()
21 24 params[CONTEXT_BUMPABLE] = bumpable
22 25 max_posts = thread.max_posts
23 26 if bumpable and thread.has_post_limit():
24 27 left_posts = max_posts - thread.get_reply_count()
25 28 params[CONTEXT_POSTS_LEFT] = left_posts
26 29 params[CONTEXT_BUMPLIMIT_PRG] = str(
27 30 float(left_posts) / max_posts * 100)
28 31
29 32 params[CONTEXT_OP] = thread.get_opening_post()
30 33
31 34 return params
@@ -1,131 +1,136 b''
1 1 from django.core.exceptions import ObjectDoesNotExist
2 2 from django.http import Http404
3 3 from django.shortcuts import get_object_or_404, render, redirect
4 4 from django.views.generic.edit import FormMixin
5 5 from django.utils import timezone
6 6 from django.utils.dateformat import format
7 7
8 8 from boards import utils, settings
9 9 from boards.forms import PostForm, PlainErrorList
10 10 from boards.models import Post
11 11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 12 from boards.views.posting_mixin import PostMixin
13 13
14 14 import neboard
15 15
16 16
17 17 CONTEXT_LASTUPDATE = "last_update"
18 18 CONTEXT_THREAD = 'thread'
19 19 CONTEXT_WS_TOKEN = 'ws_token'
20 20 CONTEXT_WS_PROJECT = 'ws_project'
21 21 CONTEXT_WS_HOST = 'ws_host'
22 22 CONTEXT_WS_PORT = 'ws_port'
23 23 CONTEXT_WS_TIME = 'ws_token_time'
24 CONTEXT_MODE = 'mode'
24 25
25 26 FORM_TITLE = 'title'
26 27 FORM_TEXT = 'text'
27 28 FORM_IMAGE = 'image'
28 29 FORM_THREADS = 'threads'
29 30
30 31
31 32 class ThreadView(BaseBoardView, PostMixin, FormMixin):
32 33
33 34 def get(self, request, post_id, form: PostForm=None):
34 35 try:
35 36 opening_post = Post.objects.get(id=post_id)
36 37 except ObjectDoesNotExist:
37 38 raise Http404
38 39
39 40 # If this is not OP, don't show it as it is
40 41 if not opening_post.is_opening():
41 42 return redirect(opening_post.get_thread().get_opening_post()
42 43 .get_absolute_url())
43 44
44 45 if not form:
45 46 form = PostForm(error_class=PlainErrorList)
46 47
47 48 thread_to_show = opening_post.get_thread()
48 49
49 50 params = dict()
50 51
51 52 params[CONTEXT_FORM] = form
52 53 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
53 54 params[CONTEXT_THREAD] = thread_to_show
55 params[CONTEXT_MODE] = self.get_mode()
54 56
55 57 if settings.get_bool('External', 'WebsocketsEnabled'):
56 58 token_time = format(timezone.now(), u'U')
57 59
58 60 params[CONTEXT_WS_TIME] = token_time
59 61 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
60 62 timestamp=token_time)
61 63 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
62 64 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
63 65 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
64 66
65 67 params.update(self.get_data(thread_to_show))
66 68
67 69 return render(request, self.get_template(), params)
68 70
69 71 def post(self, request, post_id):
70 72 opening_post = get_object_or_404(Post, id=post_id)
71 73
72 74 # If this is not OP, don't show it as it is
73 75 if not opening_post.is_opening():
74 76 raise Http404
75 77
76 78 if not opening_post.get_thread().archived:
77 79 form = PostForm(request.POST, request.FILES,
78 80 error_class=PlainErrorList)
79 81 form.session = request.session
80 82
81 83 if form.is_valid():
82 84 return self.new_post(request, form, opening_post)
83 85 if form.need_to_ban:
84 86 # Ban user because he is suspected to be a bot
85 87 self._ban_current_user(request)
86 88
87 89 return self.get(request, post_id, form)
88 90
89 91 def new_post(self, request, form: PostForm, opening_post: Post=None,
90 92 html_response=True):
91 93 """
92 94 Adds a new post (in thread or as a reply).
93 95 """
94 96
95 97 ip = utils.get_client_ip(request)
96 98
97 99 data = form.cleaned_data
98 100
99 101 title = data[FORM_TITLE]
100 102 text = data[FORM_TEXT]
101 103 image = form.get_image()
102 104 threads = data[FORM_THREADS]
103 105
104 106 text = self._remove_invalid_links(text)
105 107
106 108 post_thread = opening_post.get_thread()
107 109
108 110 post = Post.objects.create_post(title=title, text=text, image=image,
109 111 thread=post_thread, ip=ip,
110 112 threads=threads)
111 113 post.notify_clients()
112 114
113 115 if html_response:
114 116 if opening_post:
115 117 return redirect(post.get_absolute_url())
116 118 else:
117 119 return post
118 120
119 121 def get_data(self, thread):
120 122 """
121 123 Returns context params for the view.
122 124 """
123 125
124 126 pass
125 127
126 128 def get_template(self):
127 129 """
128 130 Gets template to show the thread mode on.
129 131 """
130 132
131 133 pass
134
135 def get_mode(self):
136 pass
General Comments 0
You need to be logged in to leave comments. Login now