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