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