##// END OF EJS Templates
Added timezone support (time zone is selected in settings)
neko259 -
r1065:eb6bb3e8 default
parent child Browse files
Show More
@@ -1,338 +1,346 b''
1 1 import re
2 2 import time
3 import pytz
3 4
4 5 from django import forms
5 6 from django.core.files.uploadedfile import SimpleUploadedFile
6 7 from django.forms.util import ErrorList
7 8 from django.utils.translation import ugettext_lazy as _
8 9 import requests
9 10
10 11 from boards.mdx_neboard import formatters
11 12 from boards.models.post import TITLE_MAX_LENGTH
12 13 from boards.models import Tag
13 14 from neboard import settings
14 15 import boards.settings as board_settings
15 16
16 17
17 18 CONTENT_TYPE_IMAGE = (
18 19 'image/jpeg',
19 20 'image/png',
20 21 'image/gif',
21 22 'image/bmp',
22 23 )
23 24
24 25 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
25 26
26 27 VETERAN_POSTING_DELAY = 5
27 28
28 29 ATTRIBUTE_PLACEHOLDER = 'placeholder'
29 30 ATTRIBUTE_ROWS = 'rows'
30 31
31 32 LAST_POST_TIME = 'last_post_time'
32 33 LAST_LOGIN_TIME = 'last_login_time'
33 34 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
34 35 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
35 36
36 37 LABEL_TITLE = _('Title')
37 38 LABEL_TEXT = _('Text')
38 39 LABEL_TAG = _('Tag')
39 40 LABEL_SEARCH = _('Search')
40 41
41 42 TAG_MAX_LENGTH = 20
42 43
43 44 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
44 45
45 46 HTTP_RESULT_OK = 200
46 47
47 48 TEXTAREA_ROWS = 4
48 49
49 50
51 def get_timezones():
52 timezones = []
53 for tz in pytz.common_timezones:
54 timezones.append((tz, tz),)
55 return timezones
56
57
50 58 class FormatPanel(forms.Textarea):
51 59 """
52 60 Panel for text formatting. Consists of buttons to add different tags to the
53 61 form text area.
54 62 """
55 63
56 64 def render(self, name, value, attrs=None):
57 65 output = '<div id="mark-panel">'
58 66 for formatter in formatters:
59 67 output += '<span class="mark_btn"' + \
60 68 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
61 69 '\', \'' + formatter.format_right + '\')">' + \
62 70 formatter.preview_left + formatter.name + \
63 71 formatter.preview_right + '</span>'
64 72
65 73 output += '</div>'
66 74 output += super(FormatPanel, self).render(name, value, attrs=None)
67 75
68 76 return output
69 77
70 78
71 79 class PlainErrorList(ErrorList):
72 80 def __unicode__(self):
73 81 return self.as_text()
74 82
75 83 def as_text(self):
76 84 return ''.join(['(!) %s ' % e for e in self])
77 85
78 86
79 87 class NeboardForm(forms.Form):
80 88 """
81 89 Form with neboard-specific formatting.
82 90 """
83 91
84 92 def as_div(self):
85 93 """
86 94 Returns this form rendered as HTML <as_div>s.
87 95 """
88 96
89 97 return self._html_output(
90 98 # TODO Do not show hidden rows in the list here
91 99 normal_row='<div class="form-row"><div class="form-label">'
92 100 '%(label)s'
93 101 '</div></div>'
94 102 '<div class="form-row"><div class="form-input">'
95 103 '%(field)s'
96 104 '</div></div>'
97 105 '<div class="form-row">'
98 106 '%(help_text)s'
99 107 '</div>',
100 108 error_row='<div class="form-row">'
101 109 '<div class="form-label"></div>'
102 110 '<div class="form-errors">%s</div>'
103 111 '</div>',
104 112 row_ender='</div>',
105 113 help_text_html='%s',
106 114 errors_on_separate_row=True)
107 115
108 116 def as_json_errors(self):
109 117 errors = []
110 118
111 119 for name, field in list(self.fields.items()):
112 120 if self[name].errors:
113 121 errors.append({
114 122 'field': name,
115 123 'errors': self[name].errors.as_text(),
116 124 })
117 125
118 126 return errors
119 127
120 128
121 129 class PostForm(NeboardForm):
122 130
123 131 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
124 132 label=LABEL_TITLE)
125 133 text = forms.CharField(
126 134 widget=FormatPanel(attrs={
127 135 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
128 136 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
129 137 }),
130 138 required=False, label=LABEL_TEXT)
131 139 image = forms.ImageField(required=False, label=_('Image'),
132 140 widget=forms.ClearableFileInput(
133 141 attrs={'accept': 'image/*'}))
134 142 image_url = forms.CharField(required=False, label=_('Image URL'),
135 143 widget=forms.TextInput(
136 144 attrs={ATTRIBUTE_PLACEHOLDER:
137 145 'http://example.com/image.png'}))
138 146
139 147 # This field is for spam prevention only
140 148 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
141 149 widget=forms.TextInput(attrs={
142 150 'class': 'form-email'}))
143 151
144 152 session = None
145 153 need_to_ban = False
146 154
147 155 def clean_title(self):
148 156 title = self.cleaned_data['title']
149 157 if title:
150 158 if len(title) > TITLE_MAX_LENGTH:
151 159 raise forms.ValidationError(_('Title must have less than %s '
152 160 'characters') %
153 161 str(TITLE_MAX_LENGTH))
154 162 return title
155 163
156 164 def clean_text(self):
157 165 text = self.cleaned_data['text'].strip()
158 166 if text:
159 167 if len(text) > board_settings.MAX_TEXT_LENGTH:
160 168 raise forms.ValidationError(_('Text must have less than %s '
161 169 'characters') %
162 170 str(board_settings
163 171 .MAX_TEXT_LENGTH))
164 172 return text
165 173
166 174 def clean_image(self):
167 175 image = self.cleaned_data['image']
168 176
169 177 if image:
170 178 self.validate_image_size(image.size)
171 179
172 180 return image
173 181
174 182 def clean_image_url(self):
175 183 url = self.cleaned_data['image_url']
176 184
177 185 image = None
178 186 if url:
179 187 image = self._get_image_from_url(url)
180 188
181 189 if not image:
182 190 raise forms.ValidationError(_('Invalid URL'))
183 191 else:
184 192 self.validate_image_size(image.size)
185 193
186 194 return image
187 195
188 196 def clean(self):
189 197 cleaned_data = super(PostForm, self).clean()
190 198
191 199 if not self.session:
192 200 raise forms.ValidationError('Humans have sessions')
193 201
194 202 if cleaned_data['email']:
195 203 self.need_to_ban = True
196 204 raise forms.ValidationError('A human cannot enter a hidden field')
197 205
198 206 if not self.errors:
199 207 self._clean_text_image()
200 208
201 209 if not self.errors and self.session:
202 210 self._validate_posting_speed()
203 211
204 212 return cleaned_data
205 213
206 214 def get_image(self):
207 215 """
208 216 Gets image from file or URL.
209 217 """
210 218
211 219 image = self.cleaned_data['image']
212 220 return image if image else self.cleaned_data['image_url']
213 221
214 222 def _clean_text_image(self):
215 223 text = self.cleaned_data.get('text')
216 224 image = self.get_image()
217 225
218 226 if (not text) and (not image):
219 227 error_message = _('Either text or image must be entered.')
220 228 self._errors['text'] = self.error_class([error_message])
221 229
222 230 def _validate_posting_speed(self):
223 231 can_post = True
224 232
225 233 posting_delay = settings.POSTING_DELAY
226 234
227 235 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
228 236 self.session:
229 237 now = time.time()
230 238 last_post_time = self.session[LAST_POST_TIME]
231 239
232 240 current_delay = int(now - last_post_time)
233 241
234 242 if current_delay < posting_delay:
235 243 error_message = _('Wait %s seconds after last posting') % str(
236 244 posting_delay - current_delay)
237 245 self._errors['text'] = self.error_class([error_message])
238 246
239 247 can_post = False
240 248
241 249 if can_post:
242 250 self.session[LAST_POST_TIME] = time.time()
243 251
244 252 def validate_image_size(self, size: int):
245 253 if size > board_settings.MAX_IMAGE_SIZE:
246 254 raise forms.ValidationError(
247 255 _('Image must be less than %s bytes')
248 256 % str(board_settings.MAX_IMAGE_SIZE))
249 257
250 258 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
251 259 """
252 260 Gets an image file from URL.
253 261 """
254 262
255 263 img_temp = None
256 264
257 265 try:
258 266 # Verify content headers
259 267 response_head = requests.head(url, verify=False)
260 268 content_type = response_head.headers['content-type'].split(';')[0]
261 269 if content_type in CONTENT_TYPE_IMAGE:
262 270 length_header = response_head.headers.get('content-length')
263 271 if length_header:
264 272 length = int(length_header)
265 273 self.validate_image_size(length)
266 274 # Get the actual content into memory
267 275 response = requests.get(url, verify=False, stream=True)
268 276
269 277 # Download image, stop if the size exceeds limit
270 278 size = 0
271 279 content = b''
272 280 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
273 281 size += len(chunk)
274 282 self.validate_image_size(size)
275 283 content += chunk
276 284
277 285 if response.status_code == HTTP_RESULT_OK and content:
278 286 # Set a dummy file name that will be replaced
279 287 # anyway, just keep the valid extension
280 288 filename = 'image.' + content_type.split('/')[1]
281 289 img_temp = SimpleUploadedFile(filename, content,
282 290 content_type)
283 291 except Exception:
284 292 # Just return no image
285 293 pass
286 294
287 295 return img_temp
288 296
289 297
290 298 class ThreadForm(PostForm):
291 299
292 300 tags = forms.CharField(
293 301 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
294 302 max_length=100, label=_('Tags'), required=True)
295 303
296 304 def clean_tags(self):
297 305 tags = self.cleaned_data['tags'].strip()
298 306
299 307 if not tags or not REGEX_TAGS.match(tags):
300 308 raise forms.ValidationError(
301 309 _('Inappropriate characters in tags.'))
302 310
303 311 required_tag_exists = False
304 312 for tag in tags.split():
305 313 tag_model = Tag.objects.filter(name=tag.strip().lower(),
306 314 required=True)
307 315 if tag_model.exists():
308 316 required_tag_exists = True
309 317 break
310 318
311 319 if not required_tag_exists:
312 320 raise forms.ValidationError(_('Need at least 1 required tag.'))
313 321
314 322 return tags
315 323
316 324 def clean(self):
317 325 cleaned_data = super(ThreadForm, self).clean()
318 326
319 327 return cleaned_data
320 328
321 329
322 330 class SettingsForm(NeboardForm):
323 331
324 theme = forms.ChoiceField(choices=settings.THEMES,
325 label=_('Theme'))
332 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
326 333 username = forms.CharField(label=_('User name'), required=False)
334 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
327 335
328 336 def clean_username(self):
329 337 username = self.cleaned_data['username']
330 338
331 339 if username and not REGEX_TAGS.match(username):
332 340 raise forms.ValidationError(_('Inappropriate characters.'))
333 341
334 342 return username
335 343
336 344
337 345 class SearchForm(NeboardForm):
338 346 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
1 NO CONTENT: modified file, binary diff hidden
@@ -1,377 +1,381 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 msgid ""
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2015-03-29 16:19+0300\n"
10 "POT-Creation-Date: 2015-03-30 18:42+0300\n"
11 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 14 "Language: ru\n"
15 15 "MIME-Version: 1.0\n"
16 16 "Content-Type: text/plain; charset=UTF-8\n"
17 17 "Content-Transfer-Encoding: 8bit\n"
18 18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 20
21 21 #: admin.py:22
22 22 msgid "{} posters were banned"
23 23 msgstr ""
24 24
25 25 #: authors.py:9
26 26 msgid "author"
27 27 msgstr "Π°Π²Ρ‚ΠΎΡ€"
28 28
29 29 #: authors.py:10
30 30 msgid "developer"
31 31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
32 32
33 33 #: authors.py:11
34 34 msgid "javascript developer"
35 35 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
36 36
37 37 #: authors.py:12
38 38 msgid "designer"
39 39 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
40 40
41 #: forms.py:33
41 #: forms.py:34
42 42 msgid "Type message here. Use formatting panel for more advanced usage."
43 43 msgstr ""
44 44 "Π’Π²ΠΎΠ΄ΠΈΡ‚Π΅ сообщСниС сюда. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ панСль для Π±ΠΎΠ»Π΅Π΅ слоТного форматирования."
45 45
46 #: forms.py:34
46 #: forms.py:35
47 47 msgid "tag1 several_words_tag"
48 48 msgstr "ΠΌΠ΅Ρ‚ΠΊΠ°1 ΠΌΠ΅Ρ‚ΠΊΠ°_ΠΈΠ·_Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ…_слов"
49 49
50 #: forms.py:36
50 #: forms.py:37
51 51 msgid "Title"
52 52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53 53
54 #: forms.py:37
54 #: forms.py:38
55 55 msgid "Text"
56 56 msgstr "ВСкст"
57 57
58 #: forms.py:38
58 #: forms.py:39
59 59 msgid "Tag"
60 60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
61 61
62 #: forms.py:39 templates/boards/base.html:36 templates/search/search.html:13
62 #: forms.py:40 templates/boards/base.html:36 templates/search/search.html:13
63 63 #: templates/search/search.html.py:17
64 64 msgid "Search"
65 65 msgstr "Поиск"
66 66
67 #: forms.py:131
67 #: forms.py:139
68 68 msgid "Image"
69 69 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
70 70
71 #: forms.py:134
71 #: forms.py:142
72 72 msgid "Image URL"
73 73 msgstr "URL изобраТСния"
74 74
75 #: forms.py:140
75 #: forms.py:148
76 76 msgid "e-mail"
77 77 msgstr ""
78 78
79 #: forms.py:151
79 #: forms.py:159
80 80 #, python-format
81 81 msgid "Title must have less than %s characters"
82 82 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
83 83
84 #: forms.py:160
84 #: forms.py:168
85 85 #, python-format
86 86 msgid "Text must have less than %s characters"
87 87 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
88 88
89 #: forms.py:182
89 #: forms.py:190
90 90 msgid "Invalid URL"
91 91 msgstr "НСвСрный URL"
92 92
93 #: forms.py:219
93 #: forms.py:227
94 94 msgid "Either text or image must be entered."
95 95 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
96 96
97 #: forms.py:235
97 #: forms.py:243
98 98 #, python-format
99 99 msgid "Wait %s seconds after last posting"
100 100 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
101 101
102 #: forms.py:247
102 #: forms.py:255
103 103 #, python-format
104 104 msgid "Image must be less than %s bytes"
105 105 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
106 106
107 #: forms.py:294 templates/boards/rss/post.html:10 templates/boards/tags.html:7
107 #: forms.py:302 templates/boards/rss/post.html:10 templates/boards/tags.html:7
108 108 msgid "Tags"
109 109 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ"
110 110
111 #: forms.py:301
111 #: forms.py:309
112 112 msgid "Inappropriate characters in tags."
113 113 msgstr "НСдопустимыС символы Π² ΠΌΠ΅Ρ‚ΠΊΠ°Ρ…."
114 114
115 #: forms.py:312
115 #: forms.py:320
116 116 msgid "Need at least 1 required tag."
117 117 msgstr "НуТна хотя Π±Ρ‹ 1 ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Π°Ρ ΠΌΠ΅Ρ‚ΠΊΠ°."
118 118
119 #: forms.py:325
119 #: forms.py:332
120 120 msgid "Theme"
121 121 msgstr "Π’Π΅ΠΌΠ°"
122 122
123 #: forms.py:326
123 #: forms.py:333
124 124 msgid "User name"
125 125 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
126 126
127 #: forms.py:332
127 #: forms.py:334
128 msgid "Time zone"
129 msgstr "Часовой пояс"
130
131 #: forms.py:340
128 132 msgid "Inappropriate characters."
129 133 msgstr "НСдопустимыС символы."
130 134
131 135 #: templates/boards/404.html:6
132 136 msgid "Not found"
133 137 msgstr "НС найдСно"
134 138
135 139 #: templates/boards/404.html:12
136 140 msgid "This page does not exist"
137 141 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
138 142
139 143 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
140 144 msgid "Authors"
141 145 msgstr "Авторы"
142 146
143 147 #: templates/boards/authors.html:26
144 148 msgid "Distributed under the"
145 149 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
146 150
147 151 #: templates/boards/authors.html:28
148 152 msgid "license"
149 153 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
150 154
151 155 #: templates/boards/authors.html:30
152 156 msgid "Repository"
153 157 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
154 158
155 159 #: templates/boards/base.html:13
156 160 msgid "Feed"
157 161 msgstr "Π›Π΅Π½Ρ‚Π°"
158 162
159 163 #: templates/boards/base.html:30
160 164 msgid "All threads"
161 165 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
162 166
163 167 #: templates/boards/base.html:34
164 168 msgid "Tag management"
165 169 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠ°ΠΌΠΈ"
166 170
167 171 #: templates/boards/base.html:39 templates/boards/base.html.py:40
168 172 #: templates/boards/notifications.html:8
169 173 msgid "Notifications"
170 174 msgstr "УвСдомлСния"
171 175
172 #: templates/boards/base.html:47 templates/boards/settings.html:8
176 #: templates/boards/base.html:47 templates/boards/settings.html:9
173 177 msgid "Settings"
174 178 msgstr "Настройки"
175 179
176 180 #: templates/boards/base.html:60
177 181 msgid "Admin"
178 182 msgstr "АдминистрированиС"
179 183
180 184 #: templates/boards/base.html:62
181 185 #, python-format
182 186 msgid "Speed: %(ppd)s posts per day"
183 187 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
184 188
185 189 #: templates/boards/base.html:64
186 190 msgid "Up"
187 191 msgstr "Π’Π²Π΅Ρ€Ρ…"
188 192
189 193 #: templates/boards/notifications.html:17
190 #: templates/boards/posting_general.html:79 templates/search/search.html:26
194 #: templates/boards/posting_general.html:81 templates/search/search.html:26
191 195 msgid "Previous page"
192 196 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
193 197
194 198 #: templates/boards/notifications.html:27
195 #: templates/boards/posting_general.html:119 templates/search/search.html:37
199 #: templates/boards/posting_general.html:121 templates/search/search.html:37
196 200 msgid "Next page"
197 201 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
198 202
199 203 #: templates/boards/post.html:32
200 204 msgid "Open"
201 205 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
202 206
203 207 #: templates/boards/post.html:34 templates/boards/post.html.py:38
204 208 msgid "Reply"
205 209 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
206 210
207 211 #: templates/boards/post.html:43
208 212 msgid "Edit"
209 213 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
210 214
211 215 #: templates/boards/post.html:45
212 216 msgid "Edit thread"
213 217 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
214 218
215 219 #: templates/boards/post.html:75
216 220 msgid "Replies"
217 221 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
218 222
219 #: templates/boards/post.html:86 templates/boards/thread.html:28
223 #: templates/boards/post.html:86 templates/boards/thread.html:30
220 224 msgid "messages"
221 225 msgstr "сообщСний"
222 226
223 #: templates/boards/post.html:87 templates/boards/thread.html:29
227 #: templates/boards/post.html:87 templates/boards/thread.html:31
224 228 msgid "images"
225 229 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
226 230
227 #: templates/boards/posting_general.html:63
231 #: templates/boards/posting_general.html:65
228 232 msgid "Edit tag"
229 233 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΡƒ"
230 234
231 #: templates/boards/posting_general.html:66
235 #: templates/boards/posting_general.html:68
232 236 #, python-format
233 237 msgid "This tag has %(thread_count)s threads and %(post_count)s posts."
234 238 msgstr "Π‘ этой ΠΌΠ΅Ρ‚ΠΊΠΎΠΉ Π΅ΡΡ‚ΡŒ %(thread_count)s Ρ‚Π΅ΠΌ ΠΈ %(post_count)s сообщСний."
235 239
236 #: templates/boards/posting_general.html:94
240 #: templates/boards/posting_general.html:96
237 241 #, python-format
238 242 msgid "Skipped %(count)s replies. Open thread to see all replies."
239 243 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
240 244
241 #: templates/boards/posting_general.html:124
245 #: templates/boards/posting_general.html:126
242 246 msgid "No threads exist. Create the first one!"
243 247 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
244 248
245 #: templates/boards/posting_general.html:130
249 #: templates/boards/posting_general.html:132
246 250 msgid "Create new thread"
247 251 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
248 252
249 #: templates/boards/posting_general.html:135 templates/boards/preview.html:16
250 #: templates/boards/thread_normal.html:44
253 #: templates/boards/posting_general.html:137 templates/boards/preview.html:16
254 #: templates/boards/thread_normal.html:46
251 255 msgid "Post"
252 256 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
253 257
254 #: templates/boards/posting_general.html:141
258 #: templates/boards/posting_general.html:143
255 259 msgid "Tags must be delimited by spaces. Text or image is required."
256 260 msgstr ""
257 261 "ΠœΠ΅Ρ‚ΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
258 262
259 #: templates/boards/posting_general.html:144
260 #: templates/boards/thread_normal.html:50
263 #: templates/boards/posting_general.html:146
264 #: templates/boards/thread_normal.html:51
261 265 msgid "Text syntax"
262 266 msgstr "Бинтаксис тСкста"
263 267
264 #: templates/boards/posting_general.html:156
268 #: templates/boards/posting_general.html:158
265 269 msgid "Pages:"
266 270 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
267 271
268 272 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20
269 273 msgid "Preview"
270 274 msgstr "ΠŸΡ€Π΅Π΄ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
271 275
272 276 #: templates/boards/rss/post.html:5
273 277 msgid "Post image"
274 278 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
275 279
276 #: templates/boards/settings.html:16
280 #: templates/boards/settings.html:17
277 281 msgid "You are moderator."
278 282 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
279 283
280 #: templates/boards/settings.html:20
284 #: templates/boards/settings.html:21
281 285 msgid "Hidden tags:"
282 286 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
283 287
284 #: templates/boards/settings.html:28
288 #: templates/boards/settings.html:29
285 289 msgid "No hidden tags."
286 290 msgstr "НСт скрытых ΠΌΠ΅Ρ‚ΠΎΠΊ."
287 291
288 #: templates/boards/settings.html:37
292 #: templates/boards/settings.html:38
289 293 msgid "Save"
290 294 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
291 295
292 296 #: templates/boards/staticpages/banned.html:6
293 297 msgid "Banned"
294 298 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
295 299
296 300 #: templates/boards/staticpages/banned.html:11
297 301 msgid "Your IP address has been banned. Contact the administrator"
298 302 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
299 303
300 304 #: templates/boards/staticpages/help.html:6
301 305 #: templates/boards/staticpages/help.html:10
302 306 msgid "Syntax"
303 307 msgstr "Бинтаксис"
304 308
305 309 #: templates/boards/staticpages/help.html:11
306 310 msgid "Italic text"
307 311 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
308 312
309 313 #: templates/boards/staticpages/help.html:12
310 314 msgid "Bold text"
311 315 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
312 316
313 317 #: templates/boards/staticpages/help.html:13
314 318 msgid "Spoiler"
315 319 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
316 320
317 321 #: templates/boards/staticpages/help.html:14
318 322 msgid "Link to a post"
319 323 msgstr "Бсылка Π½Π° сообщСниС"
320 324
321 325 #: templates/boards/staticpages/help.html:15
322 326 msgid "Add post to this thread"
323 327 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ сообщСниС Π² эту Ρ‚Π΅ΠΌΡƒ"
324 328
325 329 #: templates/boards/staticpages/help.html:16
326 330 msgid "Strikethrough text"
327 331 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
328 332
329 333 #: templates/boards/staticpages/help.html:17
330 334 msgid "Comment"
331 335 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
332 336
333 337 #: templates/boards/staticpages/help.html:18
334 338 msgid "Quote"
335 339 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
336 340
337 341 #: templates/boards/staticpages/help.html:20
338 342 msgid "You can try pasting the text and previewing the result here:"
339 343 msgstr "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ тСкст ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ здСсь:"
340 344
341 345 #: templates/boards/tags.html:23
342 346 msgid "No tags found."
343 347 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
344 348
345 #: templates/boards/thread.html:30
349 #: templates/boards/thread.html:32
346 350 msgid "Last update: "
347 351 msgstr "ПослСднСС обновлСниС: "
348 352
349 #: templates/boards/thread_gallery.html:20
350 #: templates/boards/thread_normal.html:14
353 #: templates/boards/thread_gallery.html:21
354 #: templates/boards/thread_normal.html:16
351 355 msgid "Normal mode"
352 356 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
353 357
354 #: templates/boards/thread_gallery.html:21
355 #: templates/boards/thread_normal.html:15
358 #: templates/boards/thread_gallery.html:22
359 #: templates/boards/thread_normal.html:17
356 360 msgid "Gallery mode"
357 361 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
358 362
359 #: templates/boards/thread_gallery.html:51
363 #: templates/boards/thread_gallery.html:52
360 364 msgid "No images."
361 365 msgstr "НСт ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ."
362 366
363 #: templates/boards/thread_normal.html:23
367 #: templates/boards/thread_normal.html:25
364 368 msgid "posts to bumplimit"
365 369 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
366 370
367 #: templates/boards/thread_normal.html:37
371 #: templates/boards/thread_normal.html:39
368 372 msgid "Reply to thread"
369 373 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
370 374
371 #: templates/boards/thread_normal.html:51
375 #: templates/boards/thread_normal.html:52
372 376 msgid "Close form"
373 377 msgstr "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ"
374 378
375 #: templates/boards/thread_normal.html:67
379 #: templates/boards/thread_normal.html:68
376 380 msgid "Update"
377 381 msgstr "ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ"
@@ -1,28 +1,42 b''
1 import pytz
2
1 3 from django.shortcuts import redirect
4 from django.utils import timezone
5
2 6 from boards import utils
3 7 from boards.models import Ban
4 8
5 9 RESPONSE_CONTENT_TYPE = 'Content-Type'
6 10
7 11 TYPE_HTML = 'text/html'
8 12
9 13
10 14 class BanMiddleware:
11 15 """
12 16 This is run before showing the thread. Banned users don't need to see
13 17 anything
14 18 """
15 19
16 20 def __init__(self):
17 21 pass
18 22
19 23 def process_view(self, request, view_func, view_args, view_kwargs):
20 24
21 25 if request.path != '/banned/':
22 26 ip = utils.get_client_ip(request)
23 27 bans = Ban.objects.filter(ip=ip)
24 28
25 29 if bans.exists():
26 30 ban = bans[0]
27 31 if not ban.can_read:
28 32 return redirect('banned')
33
34
35 class TimezoneMiddleware(object):
36 def process_request(self, request):
37 tzname = request.session.get('django_timezone')
38 if tzname:
39 timezone.activate(pytz.timezone(tzname))
40 else:
41 timezone.deactivate()
42
@@ -1,99 +1,56 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 if (window.Intl) {
27 var LOCALE = window.navigator.language;
28 var FORMATTER = new Intl.DateTimeFormat(
29 LOCALE,
30 {
31 weekday: 'short', year: 'numeric', month: 'short', day: 'numeric',
32 hour: 'numeric', minute: '2-digit', second: '2-digit'
33 }
34 );
35 }
36
37 26 /**
38 27 * An email is a hidden file to prevent spam bots from posting. It has to be
39 28 * hidden.
40 29 */
41 30 function hideEmailFromForm() {
42 31 $('.form-email').parent().parent().hide();
43 32 }
44 33
45 34 /**
46 35 * Highlight code blocks with code highlighter
47 36 */
48 37 function highlightCode(node) {
49 38 node.find('pre code').each(function(i, e) {
50 39 hljs.highlightBlock(e);
51 40 });
52 41 }
53 42
54 /**
55 * Translate timestamps to local ones for all <time> tags inside node.
56 */
57 function translate_time(node) {
58 if (window.Intl === null) {
59 return;
60 }
61
62 var elements;
63
64 if (node === null) {
65 elements = $('time');
66 } else {
67 elements = node.find('time');
68 }
69
70 if (!elements.length) {
71 return;
72 }
73
74 elements.each(function() {
75 var element = $(this);
76 var dateAttr = element.attr('datetime');
77 if (dateAttr) {
78 var date = new Date(dateAttr);
79 element.text(FORMATTER.format(date));
80 }
81 });
82 }
83
84 43 $( document ).ready(function() {
85 44 hideEmailFromForm();
86 45
87 46 $("a[href='#top']").click(function() {
88 47 $("html, body").animate({ scrollTop: 0 }, "slow");
89 48 return false;
90 49 });
91 50
92 51 addImgPreview();
93 52
94 53 addRefLinkPreview();
95 54
96 55 highlightCode($(document));
97
98 translate_time(null);
99 56 });
@@ -1,121 +1,120 b''
1 1 function $X(path, root) {
2 2 return document.evaluate(path, root || document, null, 6, null);
3 3 }
4 4 function $x(path, root) {
5 5 return document.evaluate(path, root || document, null, 8, null).singleNodeValue;
6 6 }
7 7
8 8 function $del(el) {
9 9 if(el) el.parentNode.removeChild(el);
10 10 }
11 11
12 12 function $each(list, fn) {
13 13 if(!list) return;
14 14 var i = list.snapshotLength;
15 15 if(i > 0) while(i--) fn(list.snapshotItem(i), i);
16 16 }
17 17
18 18 function mkPreview(cln, html) {
19 19 cln.innerHTML = html;
20 20
21 21 highlightCode($(cln));
22 22 addRefLinkPreview(cln);
23 translate_time($(cln));
24 23 };
25 24
26 25 function isElementInViewport (el) {
27 26 //special bonus for those using jQuery
28 27 if (typeof jQuery === "function" && el instanceof jQuery) {
29 28 el = el[0];
30 29 }
31 30
32 31 var rect = el.getBoundingClientRect();
33 32
34 33 return (
35 34 rect.top >= 0 &&
36 35 rect.left >= 0 &&
37 36 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
38 37 rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
39 38 );
40 39 }
41 40
42 41 function addRefLinkPreview(node) {
43 42 $each($X('.//a[starts-with(text(),">>")]', node || document), function(link) {
44 43 link.addEventListener('mouseover', showPostPreview, false);
45 44 link.addEventListener('mouseout', delPostPreview, false);
46 45 });
47 46 }
48 47
49 48 function showPostPreview(e) {
50 49 var doc = document;
51 50 //ref id
52 51 var pNum = $(this).text().match(/\d+/);
53 52
54 53 if (pNum == null || pNum.length == 0) {
55 54 return;
56 55 }
57 56
58 57 var post = $('#' + pNum);
59 58 if (post.length > 0 && isElementInViewport(post)) {
60 59 post.addClass('highlight');
61 60 } else {
62 61 var x = e.clientX + (doc.documentElement.scrollLeft || doc.body.scrollLeft) + 2;
63 62 var y = e.clientY + (doc.documentElement.scrollTop || doc.body.scrollTop);
64 63
65 64 var cln = doc.createElement('div');
66 65 cln.id = 'pstprev_' + pNum;
67 66 cln.className = 'post_preview';
68 67
69 68 cln.style.cssText = 'top:' + y + 'px;' + (x < doc.body.clientWidth/2 ? 'left:' + x + 'px' : 'right:' + parseInt(doc.body.clientWidth - x + 1) + 'px');
70 69
71 70 cln.addEventListener('mouseout', delPostPreview, false);
72 71
73 72 cln.innerHTML = "<div class=\"post\">" + gettext('Loading...') + "</div>";
74 73
75 74 if(post.length > 0) {
76 75 var postdata = post.clone().wrap("<div/>").parent().html();
77 76
78 77 mkPreview(cln, postdata);
79 78 } else {
80 79 $.ajax({
81 80 url: '/api/post/' + pNum + '/?truncated'
82 81 })
83 82 .success(function(data) {
84 83 var postdata = $(data).wrap("<div/>").parent().html();
85 84
86 85 //make preview
87 86 mkPreview(cln, postdata);
88 87
89 88 })
90 89 .error(function() {
91 90 cln.innerHTML = "<div class=\"post\">"
92 91 + gettext('Post not found') + "</div>";
93 92 });
94 93 }
95 94
96 95 $del(doc.getElementById(cln.id));
97 96
98 97 //add preview
99 98 $(cln).fadeIn(200);
100 99 $('body').append(cln);
101 100 }
102 101 }
103 102
104 103 function delPostPreview(e) {
105 104 var el = $x('ancestor-or-self::*[starts-with(@id,"pstprev")]', e.relatedTarget);
106 105 if(!el) {
107 106 $each($X('.//div[starts-with(@id,"pstprev")]'), function(clone) {
108 107 $del(clone)
109 108 });
110 109 } else {
111 110 while(el.nextSibling) $del(el.nextSibling);
112 111 }
113 112
114 113 $('.highlight').removeClass('highlight');
115 114 }
116 115
117 116 function addPreview() {
118 117 $('.post').find('a').each(function() {
119 118 showPostPreview($(this));
120 119 });
121 120 }
@@ -1,335 +1,333 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013-2014 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 var wsUser = '';
27 27
28 28 var unreadPosts = 0;
29 29 var documentOriginalTitle = '';
30 30
31 31 // Thread ID does not change, can be stored one time
32 32 var threadId = $('div.thread').children('.post').first().attr('id');
33 33
34 34 /**
35 35 * Connect to websocket server and subscribe to thread updates. On any update we
36 36 * request a thread diff.
37 37 *
38 38 * @returns {boolean} true if connected, false otherwise
39 39 */
40 40 function connectWebsocket() {
41 41 var metapanel = $('.metapanel')[0];
42 42
43 43 var wsHost = metapanel.getAttribute('data-ws-host');
44 44 var wsPort = metapanel.getAttribute('data-ws-port');
45 45
46 46 if (wsHost.length > 0 && wsPort.length > 0)
47 47 var centrifuge = new Centrifuge({
48 48 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
49 49 "project": metapanel.getAttribute('data-ws-project'),
50 50 "user": wsUser,
51 51 "timestamp": metapanel.getAttribute('data-last-update'),
52 52 "token": metapanel.getAttribute('data-ws-token'),
53 53 "debug": false
54 54 });
55 55
56 56 centrifuge.on('error', function(error_message) {
57 57 console.log("Error connecting to websocket server.");
58 58 return false;
59 59 });
60 60
61 61 centrifuge.on('connect', function() {
62 62 var channelName = 'thread:' + threadId;
63 63 centrifuge.subscribe(channelName, function(message) {
64 64 getThreadDiff();
65 65 });
66 66
67 67 // For the case we closed the browser and missed some updates
68 68 getThreadDiff();
69 69 $('#autoupdate').hide();
70 70 });
71 71
72 72 centrifuge.connect();
73 73
74 74 return true;
75 75 }
76 76
77 77 /**
78 78 * Get diff of the posts from the current thread timestamp.
79 79 * This is required if the browser was closed and some post updates were
80 80 * missed.
81 81 */
82 82 function getThreadDiff() {
83 83 var lastUpdateTime = $('.metapanel').attr('data-last-update');
84 84
85 85 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
86 86
87 87 $.getJSON(diffUrl)
88 88 .success(function(data) {
89 89 var addedPosts = data.added;
90 90
91 91 for (var i = 0; i < addedPosts.length; i++) {
92 92 var postText = addedPosts[i];
93 93 var post = $(postText);
94 94
95 95 updatePost(post)
96 96 }
97 97
98 98 var updatedPosts = data.updated;
99 99
100 100 for (var i = 0; i < updatedPosts.length; i++) {
101 101 var postText = updatedPosts[i];
102 102 var post = $(postText);
103 103
104 104 updatePost(post)
105 105 }
106 106
107 107 // TODO Process removed posts if any
108 108 $('.metapanel').attr('data-last-update', data.last_update);
109 109 })
110 110 }
111 111
112 112 /**
113 113 * Add or update the post on html page.
114 114 */
115 115 function updatePost(postHtml) {
116 116 // This needs to be set on start because the page is scrolled after posts
117 117 // are added or updated
118 118 var bottom = isPageBottom();
119 119
120 120 var post = $(postHtml);
121 121
122 122 var threadBlock = $('div.thread');
123 123
124 124 var lastUpdate = '';
125 125
126 126 var postId = post.attr('id');
127 127
128 128 // If the post already exists, replace it. Otherwise add as a new one.
129 129 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
130 130
131 131 if (existingPosts.size() > 0) {
132 132 existingPosts.replaceWith(post);
133 133 } else {
134 134 var threadPosts = threadBlock.children('.post');
135 135 var lastPost = threadPosts.last();
136 136
137 137 post.appendTo(lastPost.parent());
138 138
139 139 updateBumplimitProgress(1);
140 140 showNewPostsTitle(1);
141 141
142 142 lastUpdate = post.children('.post-info').first()
143 143 .children('.pub_time').first().html();
144 144
145 145 if (bottom) {
146 146 scrollToBottom();
147 147 }
148 148 }
149 149
150 150 processNewPost(post);
151 151 updateMetadataPanel(lastUpdate)
152 152 }
153 153
154 154 /**
155 155 * Initiate a blinking animation on a node to show it was updated.
156 156 */
157 157 function blink(node) {
158 158 var blinkCount = 2;
159 159
160 160 var nodeToAnimate = node;
161 161 for (var i = 0; i < blinkCount; i++) {
162 162 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
163 163 }
164 164 }
165 165
166 166 function isPageBottom() {
167 167 var scroll = $(window).scrollTop() / ($(document).height()
168 168 - $(window).height());
169 169
170 170 return scroll == 1
171 171 }
172 172
173 173 function initAutoupdate() {
174 174 return connectWebsocket();
175 175 }
176 176
177 177 function getReplyCount() {
178 178 return $('.thread').children('.post').length
179 179 }
180 180
181 181 function getImageCount() {
182 182 return $('.thread').find('img').length
183 183 }
184 184
185 185 /**
186 186 * Update post count, images count and last update time in the metadata
187 187 * panel.
188 188 */
189 189 function updateMetadataPanel(lastUpdate) {
190 190 var replyCountField = $('#reply-count');
191 191 var imageCountField = $('#image-count');
192 192
193 193 replyCountField.text(getReplyCount());
194 194 imageCountField.text(getImageCount());
195 195
196 196 if (lastUpdate !== '') {
197 197 var lastUpdateField = $('#last-update');
198 198 lastUpdateField.html(lastUpdate);
199 translate_time(lastUpdateField);
200 199 blink(lastUpdateField);
201 200 }
202 201
203 202 blink(replyCountField);
204 203 blink(imageCountField);
205 204 }
206 205
207 206 /**
208 207 * Update bumplimit progress bar
209 208 */
210 209 function updateBumplimitProgress(postDelta) {
211 210 var progressBar = $('#bumplimit_progress');
212 211 if (progressBar) {
213 212 var postsToLimitElement = $('#left_to_limit');
214 213
215 214 var oldPostsToLimit = parseInt(postsToLimitElement.text());
216 215 var postCount = getReplyCount();
217 216 var bumplimit = postCount - postDelta + oldPostsToLimit;
218 217
219 218 var newPostsToLimit = bumplimit - postCount;
220 219 if (newPostsToLimit <= 0) {
221 220 $('.bar-bg').remove();
222 221 } else {
223 222 postsToLimitElement.text(newPostsToLimit);
224 223 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
225 224 }
226 225 }
227 226 }
228 227
229 228 /**
230 229 * Show 'new posts' text in the title if the document is not visible to a user
231 230 */
232 231 function showNewPostsTitle(newPostCount) {
233 232 if (document.hidden) {
234 233 if (documentOriginalTitle === '') {
235 234 documentOriginalTitle = document.title;
236 235 }
237 236 unreadPosts = unreadPosts + newPostCount;
238 237 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
239 238
240 239 document.addEventListener('visibilitychange', function() {
241 240 if (documentOriginalTitle !== '') {
242 241 document.title = documentOriginalTitle;
243 242 documentOriginalTitle = '';
244 243 unreadPosts = 0;
245 244 }
246 245
247 246 document.removeEventListener('visibilitychange', null);
248 247 });
249 248 }
250 249 }
251 250
252 251 /**
253 252 * Clear all entered values in the form fields
254 253 */
255 254 function resetForm(form) {
256 255 form.find('input:text, input:password, input:file, select, textarea').val('');
257 256 form.find('input:radio, input:checkbox')
258 257 .removeAttr('checked').removeAttr('selected');
259 258 $('.file_wrap').find('.file-thumb').remove();
260 259 }
261 260
262 261 /**
263 262 * When the form is posted, this method will be run as a callback
264 263 */
265 264 function updateOnPost(response, statusText, xhr, form) {
266 265 var json = $.parseJSON(response);
267 266 var status = json.status;
268 267
269 268 showAsErrors(form, '');
270 269
271 270 if (status === 'ok') {
272 271 resetFormPosition();
273 272 resetForm(form);
274 273 getThreadDiff();
275 274 scrollToBottom();
276 275 } else {
277 276 var errors = json.errors;
278 277 for (var i = 0; i < errors.length; i++) {
279 278 var fieldErrors = errors[i];
280 279
281 280 var error = fieldErrors.errors;
282 281
283 282 showAsErrors(form, error);
284 283 }
285 284 }
286 285 }
287 286
288 287 /**
289 288 * Show text in the errors row of the form.
290 289 * @param form
291 290 * @param text
292 291 */
293 292 function showAsErrors(form, text) {
294 293 form.children('.form-errors').remove();
295 294
296 295 if (text.length > 0) {
297 296 var errorList = $('<div class="form-errors">' + text + '<div>');
298 297 errorList.appendTo(form);
299 298 }
300 299 }
301 300
302 301 /**
303 302 * Run js methods that are usually run on the document, on the new post
304 303 */
305 304 function processNewPost(post) {
306 305 addRefLinkPreview(post[0]);
307 306 highlightCode(post);
308 translate_time(post);
309 307 blink(post);
310 308 }
311 309
312 310 $(document).ready(function(){
313 311 if (initAutoupdate()) {
314 312 // Post form data over AJAX
315 313 var threadId = $('div.thread').children('.post').first().attr('id');
316 314
317 315 var form = $('#form');
318 316
319 317 if (form.length > 0) {
320 318 var options = {
321 319 beforeSubmit: function(arr, $form, options) {
322 320 showAsErrors($('form'), gettext('Sending message...'));
323 321 },
324 322 success: updateOnPost,
325 323 url: '/api/add_post/' + threadId + '/'
326 324 };
327 325
328 326 form.ajaxForm(options);
329 327
330 328 resetForm(form);
331 329 }
332 330 }
333 331
334 332 $('#autoupdate').click(getThreadDiff);
335 333 });
@@ -1,190 +1,192 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load board %}
6 6 {% load static %}
7 {% load tz %}
7 8
8 9 {% block head %}
9 10 <meta name="robots" content="noindex">
10 11
11 12 {% if tag %}
12 13 <title>{{ tag.name }} - {{ site_name }}</title>
13 14 {% else %}
14 15 <title>{{ site_name }}</title>
15 16 {% endif %}
16 17
17 18 {% if current_page.has_previous %}
18 19 <link rel="prev" href="
19 20 {% if tag %}
20 21 {% url "tag" tag_name=tag.name page=current_page.previous_page_number %}
21 22 {% else %}
22 23 {% url "index" page=current_page.previous_page_number %}
23 24 {% endif %}
24 25 " />
25 26 {% endif %}
26 27 {% if current_page.has_next %}
27 28 <link rel="next" href="
28 29 {% if tag %}
29 30 {% url "tag" tag_name=tag.name page=current_page.next_page_number %}
30 31 {% else %}
31 32 {% url "index" page=current_page.next_page_number %}
32 33 {% endif %}
33 34 " />
34 35 {% endif %}
35 36
36 37 {% endblock %}
37 38
38 39 {% block content %}
39 40
40 41 {% get_current_language as LANGUAGE_CODE %}
42 {% get_current_timezone as TIME_ZONE %}
41 43
42 44 {% if tag %}
43 45 <div class="tag_info">
44 46 <h2>
45 47 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
46 48 {% if is_favorite %}
47 49 <button name="method" value="unsubscribe" class="fav">β˜…</button>
48 50 {% else %}
49 51 <button name="method" value="subscribe" class="not_fav">β˜…</button>
50 52 {% endif %}
51 53 </form>
52 54 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
53 55 {% if is_hidden %}
54 56 <button name="method" value="unhide" class="fav">H</button>
55 57 {% else %}
56 58 <button name="method" value="hide" class="not_fav">H</button>
57 59 {% endif %}
58 60 </form>
59 61 {% autoescape off %}
60 62 {{ tag.get_view }}
61 63 {% endautoescape %}
62 64 {% if moderator %}
63 65 <span class="moderator_info">[<a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a>]</span>
64 66 {% endif %}
65 67 </h2>
66 68 <p>{% blocktrans with thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads and {{ post_count }} posts.{% endblocktrans %}</p>
67 69 </div>
68 70 {% endif %}
69 71
70 72 {% if threads %}
71 73 {% if current_page.has_previous %}
72 74 <div class="page_link">
73 75 <a href="
74 76 {% if tag %}
75 77 {% url "tag" tag_name=tag.name page=current_page.previous_page_number %}
76 78 {% else %}
77 79 {% url "index" page=current_page.previous_page_number %}
78 80 {% endif %}
79 81 ">{% trans "Previous page" %}</a>
80 82 </div>
81 83 {% endif %}
82 84
83 85 {% for thread in threads %}
84 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
86 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE TIME_ZONE %}
85 87 <div class="thread">
86 88 {% post_view thread.get_opening_post moderator is_opening=True thread=thread truncated=True need_open_link=True %}
87 89 {% if not thread.archived %}
88 90 {% with last_replies=thread.get_last_replies %}
89 91 {% if last_replies %}
90 92 {% with skipped_replies_count=thread.get_skipped_replies_count %}
91 93 {% if skipped_replies_count %}
92 94 <div class="skipped_replies">
93 95 <a href="{% url 'thread' thread.get_opening_post_id %}">
94 96 {% blocktrans with count=skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
95 97 </a>
96 98 </div>
97 99 {% endif %}
98 100 {% endwith %}
99 101 <div class="last-replies">
100 102 {% for post in last_replies %}
101 103 {% post_view post is_opening=False moderator=moderator truncated=True %}
102 104 {% endfor %}
103 105 </div>
104 106 {% endif %}
105 107 {% endwith %}
106 108 {% endif %}
107 109 </div>
108 110 {% endcache %}
109 111 {% endfor %}
110 112
111 113 {% if current_page.has_next %}
112 114 <div class="page_link">
113 115 <a href="
114 116 {% if tag %}
115 117 {% url "tag" tag_name=tag.name page=current_page.next_page_number %}
116 118 {% else %}
117 119 {% url "index" page=current_page.next_page_number %}
118 120 {% endif %}
119 121 ">{% trans "Next page" %}</a>
120 122 </div>
121 123 {% endif %}
122 124 {% else %}
123 125 <div class="post">
124 126 {% trans 'No threads exist. Create the first one!' %}</div>
125 127 {% endif %}
126 128
127 129 <div class="post-form-w">
128 130 <script src="{% static 'js/panel.js' %}"></script>
129 131 <div class="post-form">
130 132 <div class="form-title">{% trans "Create new thread" %}</div>
131 133 <div class="swappable-form-full">
132 134 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
133 135 {{ form.as_div }}
134 136 <div class="form-submit">
135 137 <input type="submit" value="{% trans "Post" %}"/>
136 138 </div>
137 139 (ctrl-enter)
138 140 </form>
139 141 </div>
140 142 <div>
141 143 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
142 144 </div>
143 145 <div><a href="{% url "staticpage" name="help" %}">
144 146 {% trans 'Text syntax' %}</a></div>
145 147 </div>
146 148 </div>
147 149
148 150 <script src="{% static 'js/form.js' %}"></script>
149 151
150 152 {% endblock %}
151 153
152 154 {% block metapanel %}
153 155
154 156 <span class="metapanel">
155 157 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
156 158 {% trans "Pages:" %}
157 159 <a href="
158 160 {% if tag %}
159 161 {% url "tag" tag_name=tag.name page=paginator.page_range|first %}
160 162 {% else %}
161 163 {% url "index" page=paginator.page_range|first %}
162 164 {% endif %}
163 165 ">&lt;&lt;</a>
164 166 [
165 167 {% for page in paginator.center_range %}
166 168 <a
167 169 {% ifequal page current_page.number %}
168 170 class="current_page"
169 171 {% endifequal %}
170 172 href="
171 173 {% if tag %}
172 174 {% url "tag" tag_name=tag.name page=page %}
173 175 {% else %}
174 176 {% url "index" page=page %}
175 177 {% endif %}
176 178 ">{{ page }}</a>
177 179 {% if not forloop.last %},{% endif %}
178 180 {% endfor %}
179 181 ]
180 182 <a href="
181 183 {% if tag %}
182 184 {% url "tag" tag_name=tag.name page=paginator.page_range|last %}
183 185 {% else %}
184 186 {% url "index" page=paginator.page_range|last %}
185 187 {% endif %}
186 188 ">&gt;&gt;</a>
187 189 [<a href="rss/">RSS</a>]
188 190 </span>
189 191
190 192 {% endblock %}
@@ -1,43 +1,44 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load humanize %}
5 {% load tz %}
5 6
6 7 {% block head %}
7 8 <meta name="robots" content="noindex">
8 9 <title>{% trans 'Settings' %} - {{ site_name }}</title>
9 10 {% endblock %}
10 11
11 12 {% block content %}
12 13
13 14 <div class="post">
14 15 <p>
15 16 {% if moderator %}
16 17 {% trans 'You are moderator.' %}
17 18 {% endif %}
18 19 </p>
19 20 {% if hidden_tags %}
20 21 <p>{% trans 'Hidden tags:' %}
21 22 {% for tag in hidden_tags %}
22 23 {% autoescape off %}
23 24 {{ tag.get_view }}
24 25 {% endautoescape %}
25 26 {% endfor %}
26 27 </p>
27 28 {% else %}
28 29 <p>{% trans 'No hidden tags.' %}</p>
29 30 {% endif %}
30 31 </div>
31 32
32 33 <div class="post-form-w">
33 34 <div class="post-form">
34 35 <form method="post">{% csrf_token %}
35 36 {{ form.as_div }}
36 37 <div class="form-submit">
37 38 <input type="submit" value="{% trans "Save" %}" />
38 39 </div>
39 40 </form>
40 41 </div>
41 42 </div>
42 43
43 44 {% endblock %}
@@ -1,35 +1,37 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 {% load tz %}
7 8
8 9 {% block head %}
9 10 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
10 11 - {{ site_name }}</title>
11 12 {% endblock %}
12 13
13 14 {% block metapanel %}
14 15
15 16 {% get_current_language as LANGUAGE_CODE %}
17 {% get_current_timezone as TIME_ZONE %}
16 18
17 19 <span class="metapanel"
18 20 data-last-update="{{ last_update }}"
19 21 data-ws-token="{{ ws_token }}"
20 22 data-ws-project="{{ ws_project }}"
21 23 data-ws-host="{{ ws_host }}"
22 24 data-ws-port="{{ ws_port }}">
23 25
24 26 {% block thread_meta_panel %}
25 27 {% endblock %}
26 28
27 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
29 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE TIME_ZONE %}
28 30 <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %} {% trans 'messages' %},
29 31 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
30 32 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time|date:'r' }}</time></span>
31 33 [<a href="rss/">RSS</a>]
32 34 {% endcache %}
33 35 </span>
34 36
35 37 {% endblock %}
@@ -1,57 +1,56 b''
1 1 {% extends "boards/thread.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 {% load tz %}
7 8
8 9 {% block head %}
9 10 <meta name="robots" content="noindex">
10 11 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }}
11 12 - {{ site_name }}</title>
12 13 {% endblock %}
13 14
14 15 {% block content %}
15 {% spaceless %}
16 16 {% get_current_language as LANGUAGE_CODE %}
17 {% get_current_timezone as TIME_ZONE %}
17 18
18 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host %}
19 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host TIME_ZONE %}
19 20 <div class="image-mode-tab">
20 21 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
21 22 <a class="current_mode" href="{% url 'thread_gallery' thread.get_opening_post.id %}">{% trans 'Gallery mode' %}</a>
22 23 </div>
23 24
24 25 <div id="posts-table">
25 26 {% if posts %}
26 27 {% for post in posts %}
27 28 <div class="gallery_image">
28 29 {% with post.get_first_image as image %}
29 30 <div>
30 31 <a
31 32 class="thumb"
32 33 href="{{ image.image.url }}"><img
33 34 src="{{ image.image.url_200x150 }}"
34 35 alt="{{ post.id }}"
35 36 width="{{ image.pre_width }}"
36 37 height="{{ image.pre_height }}"
37 38 data-width="{{ image.width }}"
38 39 data-height="{{ image.height }}"/>
39 40 </a>
40 41 </div>
41 42 <div class="gallery_image_metadata">
42 43 {{ image.width }}x{{ image.height }}
43 44 {% image_actions image.image.url request.get_host %}
44 45 <br />
45 46 <a href="{{ post.get_url }}">>>{{ post.id }}</a>
46 47 </div>
47 48 {% endwith %}
48 49 </div>
49 50 {% endfor %}
50 51 {% else %}
51 52 {% trans 'No images.' %}
52 53 {% endif %}
53 54 </div>
54 55 {% endcache %}
55
56 {% endspaceless %}
57 56 {% endblock %}
@@ -1,67 +1,69 b''
1 1 {% extends "boards/thread.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 {% load tz %}
7 8
8 9 {% block content %}
9 10 {% get_current_language as LANGUAGE_CODE %}
11 {% get_current_timezone as TIME_ZONE %}
10 12
11 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
13 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE TIME_ZONE %}
12 14
13 15 <div class="image-mode-tab">
14 16 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
15 17 <a href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery mode' %}</a>
16 18 </div>
17 19
18 20 {% if bumpable and thread.has_post_limit %}
19 21 <div class="bar-bg">
20 22 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
21 23 </div>
22 24 <div class="bar-text">
23 25 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
24 26 </div>
25 27 </div>
26 28 {% endif %}
27 29
28 30 <div class="thread">
29 31 {% for post in thread.get_replies %}
30 32 {% post_view post moderator=moderator reply_link=True %}
31 33 {% endfor %}
32 34 </div>
33 35
34 36 {% if not thread.archived %}
35 37 <div class="post-form-w">
36 38 <script src="{% static 'js/panel.js' %}"></script>
37 39 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
38 40 <div class="post-form" id="compact-form">
39 41 <div class="swappable-form-full">
40 42 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
41 43 <div class="compact-form-text"></div>
42 44 {{ form.as_div }}
43 45 <div class="form-submit">
44 46 <input type="submit" value="{% trans "Post" %}"/>
45 47 </div>
46 48 </form>
47 49 </div>
48 50 <div><a href="{% url "staticpage" name="help" %}">
49 51 {% trans 'Text syntax' %}</a></div>
50 52 <div><a href="#" onClick="resetFormPosition(); return false;">{% trans 'Close form' %}</a></div>
51 53 </div>
52 54 </div>
53 55
54 56 <script src="{% static 'js/jquery.form.min.js' %}"></script>
55 57 {% endif %}
56 58
57 59 <script src="{% static 'js/form.js' %}"></script>
58 60 <script src="{% static 'js/thread.js' %}"></script>
59 61 <script src="{% static 'js/thread_update.js' %}"></script>
60 62 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
61 63
62 64 {% endcache %}
63 65 {% endblock %}
64 66
65 67 {% block thread_meta_panel %}
66 68 <button id="autoupdate">{% trans 'Update' %}</button>
67 69 {% endblock %}
@@ -1,61 +1,69 b''
1 import pytz
2
1 3 from django.db import transaction
2 4 from django.shortcuts import render, redirect
5 from django.utils import timezone
3 6
4 7 from boards.abstracts.settingsmanager import get_settings_manager, \
5 8 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID
6 9 from boards.views.base import BaseBoardView, CONTEXT_FORM
7 10 from boards.forms import SettingsForm, PlainErrorList
8 11
9 12 FORM_THEME = 'theme'
10 13 FORM_USERNAME = 'username'
14 FORM_TIMEZONE = 'timezone'
11 15
12 16 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
13 17
14 18 TEMPLATE = 'boards/settings.html'
15 19
16 20
17 21 class SettingsView(BaseBoardView):
18 22
19 23 def get(self, request):
20 24 params = dict()
21 25 settings_manager = get_settings_manager(request)
22 26
23 27 selected_theme = settings_manager.get_theme()
24 28
25 29 form = SettingsForm(
26 30 initial={
27 31 FORM_THEME: selected_theme,
28 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME)},
32 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME),
33 FORM_TIMEZONE: request.session.get('django_timezone', timezone.get_current_timezone()),
34 },
29 35 error_class=PlainErrorList)
30 36
31 37 params[CONTEXT_FORM] = form
32 38 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
33 39
34 40 return render(request, TEMPLATE, params)
35 41
36 42 def post(self, request):
37 43 settings_manager = get_settings_manager(request)
38 44
39 45 with transaction.atomic():
40 46 form = SettingsForm(request.POST, error_class=PlainErrorList)
41 47
42 48 if form.is_valid():
43 49 selected_theme = form.cleaned_data[FORM_THEME]
44 50 username = form.cleaned_data[FORM_USERNAME].lower()
45 51
46 52 settings_manager.set_theme(selected_theme)
47 53
48 54 old_username = settings_manager.get_setting(SETTING_USERNAME)
49 55 if username != old_username:
50 56 settings_manager.set_setting(SETTING_USERNAME, username)
51 57 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None)
52 58
59 request.session['django_timezone'] = form.cleaned_data[FORM_TIMEZONE]
60
53 61 return redirect('settings')
54 62 else:
55 63 params = dict()
56 64
57 65 params[CONTEXT_FORM] = form
58 66 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
59 67
60 68 return render(request, TEMPLATE, params)
61 69
@@ -1,233 +1,234 b''
1 1 # Django settings for neboard project.
2 2 import os
3 3 from boards.mdx_neboard import bbcode_extended
4 4
5 5 DEBUG = True
6 6 TEMPLATE_DEBUG = DEBUG
7 7
8 8 ADMINS = (
9 9 # ('Your Name', 'your_email@example.com'),
10 10 ('admin', 'admin@example.com')
11 11 )
12 12
13 13 MANAGERS = ADMINS
14 14
15 15 DATABASES = {
16 16 'default': {
17 17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 18 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 19 'USER': '', # Not used with sqlite3.
20 20 'PASSWORD': '', # Not used with sqlite3.
21 21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 23 'CONN_MAX_AGE': None,
24 24 }
25 25 }
26 26
27 27 # Local time zone for this installation. Choices can be found here:
28 28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 29 # although not all choices may be available on all operating systems.
30 30 # In a Windows environment this must be set to your system time zone.
31 31 TIME_ZONE = 'Europe/Kiev'
32 32
33 33 # Language code for this installation. All choices can be found here:
34 34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 35 LANGUAGE_CODE = 'en'
36 36
37 37 SITE_ID = 1
38 38
39 39 # If you set this to False, Django will make some optimizations so as not
40 40 # to load the internationalization machinery.
41 41 USE_I18N = True
42 42
43 43 # If you set this to False, Django will not format dates, numbers and
44 44 # calendars according to the current locale.
45 45 USE_L10N = True
46 46
47 47 # If you set this to False, Django will not use timezone-aware datetimes.
48 48 USE_TZ = True
49 49
50 50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 51 # Example: "/home/media/media.lawrence.com/media/"
52 52 MEDIA_ROOT = './media/'
53 53
54 54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 55 # trailing slash.
56 56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 57 MEDIA_URL = '/media/'
58 58
59 59 # Absolute path to the directory static files should be collected to.
60 60 # Don't put anything in this directory yourself; store your static files
61 61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 62 # Example: "/home/media/media.lawrence.com/static/"
63 63 STATIC_ROOT = ''
64 64
65 65 # URL prefix for static files.
66 66 # Example: "http://media.lawrence.com/static/"
67 67 STATIC_URL = '/static/'
68 68
69 69 # Additional locations of static files
70 70 # It is really a hack, put real paths, not related
71 71 STATICFILES_DIRS = (
72 72 os.path.dirname(__file__) + '/boards/static',
73 73
74 74 # '/d/work/python/django/neboard/neboard/boards/static',
75 75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 76 # Always use forward slashes, even on Windows.
77 77 # Don't forget to use absolute paths, not relative paths.
78 78 )
79 79
80 80 # List of finder classes that know how to find static files in
81 81 # various locations.
82 82 STATICFILES_FINDERS = (
83 83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 85 'compressor.finders.CompressorFinder',
86 86 )
87 87
88 88 if DEBUG:
89 89 STATICFILES_STORAGE = \
90 90 'django.contrib.staticfiles.storage.StaticFilesStorage'
91 91 else:
92 92 STATICFILES_STORAGE = \
93 93 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
94 94
95 95 # Make this unique, and don't share it with anybody.
96 96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
97 97
98 98 # List of callables that know how to import templates from various sources.
99 99 TEMPLATE_LOADERS = (
100 100 'django.template.loaders.filesystem.Loader',
101 101 'django.template.loaders.app_directories.Loader',
102 102 )
103 103
104 104 TEMPLATE_CONTEXT_PROCESSORS = (
105 105 'django.core.context_processors.media',
106 106 'django.core.context_processors.static',
107 107 'django.core.context_processors.request',
108 108 'django.contrib.auth.context_processors.auth',
109 109 'boards.context_processors.user_and_ui_processor',
110 110 )
111 111
112 112 MIDDLEWARE_CLASSES = (
113 113 'django.contrib.sessions.middleware.SessionMiddleware',
114 114 'django.middleware.locale.LocaleMiddleware',
115 115 'django.middleware.common.CommonMiddleware',
116 116 'django.contrib.auth.middleware.AuthenticationMiddleware',
117 117 'django.contrib.messages.middleware.MessageMiddleware',
118 118 'boards.middlewares.BanMiddleware',
119 'boards.middlewares.TimezoneMiddleware',
119 120 )
120 121
121 122 ROOT_URLCONF = 'neboard.urls'
122 123
123 124 # Python dotted path to the WSGI application used by Django's runserver.
124 125 WSGI_APPLICATION = 'neboard.wsgi.application'
125 126
126 127 TEMPLATE_DIRS = (
127 128 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
128 129 # Always use forward slashes, even on Windows.
129 130 # Don't forget to use absolute paths, not relative paths.
130 131 'templates',
131 132 )
132 133
133 134 INSTALLED_APPS = (
134 135 'django.contrib.auth',
135 136 'django.contrib.contenttypes',
136 137 'django.contrib.sessions',
137 138 # 'django.contrib.sites',
138 139 'django.contrib.messages',
139 140 'django.contrib.staticfiles',
140 141 # Uncomment the next line to enable the admin:
141 142 'django.contrib.admin',
142 143 # Uncomment the next line to enable admin documentation:
143 144 # 'django.contrib.admindocs',
144 145 'django.contrib.humanize',
145 146 'django_cleanup',
146 147
147 148 'debug_toolbar',
148 149
149 150 # Search
150 151 'haystack',
151 152
152 153 'boards',
153 154 )
154 155
155 156 # A sample logging configuration. The only tangible logging
156 157 # performed by this configuration is to send an email to
157 158 # the site admins on every HTTP 500 error when DEBUG=False.
158 159 # See http://docs.djangoproject.com/en/dev/topics/logging for
159 160 # more details on how to customize your logging configuration.
160 161 LOGGING = {
161 162 'version': 1,
162 163 'disable_existing_loggers': False,
163 164 'formatters': {
164 165 'verbose': {
165 166 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
166 167 },
167 168 'simple': {
168 169 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
169 170 },
170 171 },
171 172 'filters': {
172 173 'require_debug_false': {
173 174 '()': 'django.utils.log.RequireDebugFalse'
174 175 }
175 176 },
176 177 'handlers': {
177 178 'console': {
178 179 'level': 'DEBUG',
179 180 'class': 'logging.StreamHandler',
180 181 'formatter': 'simple'
181 182 },
182 183 },
183 184 'loggers': {
184 185 'boards': {
185 186 'handlers': ['console'],
186 187 'level': 'DEBUG',
187 188 }
188 189 },
189 190 }
190 191
191 192 HAYSTACK_CONNECTIONS = {
192 193 'default': {
193 194 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
194 195 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
195 196 },
196 197 }
197 198
198 199 THEMES = [
199 200 ('md', 'Mystic Dark'),
200 201 ('md_centered', 'Mystic Dark (centered)'),
201 202 ('sw', 'Snow White'),
202 203 ('pg', 'Photon Gray'),
203 204 ]
204 205
205 206 POSTING_DELAY = 20 # seconds
206 207
207 208 # Websocket settins
208 209 CENTRIFUGE_HOST = 'localhost'
209 210 CENTRIFUGE_PORT = '9090'
210 211
211 212 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
212 213 CENTRIFUGE_PROJECT_ID = '<project id here>'
213 214 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
214 215 CENTRIFUGE_TIMEOUT = 5
215 216
216 217 # Debug mode middlewares
217 218 if DEBUG:
218 219 MIDDLEWARE_CLASSES += (
219 220 'debug_toolbar.middleware.DebugToolbarMiddleware',
220 221 )
221 222
222 223 def custom_show_toolbar(request):
223 224 return True
224 225
225 226 DEBUG_TOOLBAR_CONFIG = {
226 227 'ENABLE_STACKTRACES': True,
227 228 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
228 229 }
229 230
230 231 # FIXME Uncommenting this fails somehow. Need to investigate this
231 232 #DEBUG_TOOLBAR_PANELS += (
232 233 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
233 234 #)
General Comments 0
You need to be logged in to leave comments. Login now